Allow file inclusion in pg_hba and pg_ident files

Started by Julien Rouhaudalmost 4 years ago96 messages
#1Julien Rouhaud
rjuju123@gmail.com
3 attachment(s)

Hi,

I recently had to work on some internal tool which, among other things, has to
merge pg_hba.conf and pg_ident.conf files between an existing (possibly
already updated) instance and some external repository.

My biggest concern is that any issue in either the external repository content
or the tool could leave the instance entirely unreachable. There are some
protection attemps to avoid that, but really there isn't any practical
possibility if one wants to make sure that some of the entries are left alone
or not hidden no matter what.

To address that, I'd like to propose the possibility to include files in hba
and ident configuration files. This was already discussed in the past, and in
my understanding this is mostly wanted, while some people expressed concerned
on a use case that wouldn't rely on thousands of entries.

In my case, there shouldn't be more than a dozen generated pg_hba/pg_ident
lines. All I want is to have my main hba (and ident) files do something like:

include hba_forbid_non_ssl.conf
include hba_superuser.conf
include hba_replication.conf
include hba_application_generated.conf

So that the tool has a way to make sure that it won't prevent dba login or
replication, or allow unsecure connections, and can simply rewrite its target
file rather than merging it, sorting it with weird rules and deciding which
lines are ok to be removed and which one aren't.

I'm attaching a patchset that implements $SUBJECT:

0001 adds a new pg_ident_file_mappings view, which is basically the same as
pg_hba_file_rules view but for mappings. It's probably already useful, for
instance if you need to tweak some regexp.

0002 adds a new "include" directive to both auth files, which will include the
given file at this exact position. I chose to do this is in the tokenization
rather than the parsing, as it makes things simpler in general, and also allows
a single code path for both files. It also adds 2 new columns to the
pg_hba_file_rules and pg_ident_file_mappings views: file_name and
(rule|mapping)_number, so it's possible to identify where a rule is coming from
and more importantly which has the priority over the over. Note that I only
added "include", but maybe people would want something like include_dir or
include_if_exists too.

Both patches have updated documentation, but no test yet. If there's an
agreement on the feature, I plan to add some TAP test for it.

Finally I also added 0003, which is a POC for a new pg_hba_matches() function,
that can help DBA to understand why their configuration isn't working as they
expect. This only to start the discussion on that topic, the code is for now
really hackish, as I don't know how much this is wanted and/or if some other
behavior would be better, and there's also no documentation or test. The
function for now only takes an optional inet (null means unix socket), the
target role and an optional ssl flag and returns the file, line and raw line
matching if any, or null. For instance:

=# select * from pg_hba_matches('127.0.0.1'::inet, 'postgres');
-[ RECORD 1 ]-----------------------------------------------------------------
file_name | /tmp/pgbuild/toto.conf
line_num | 5
raw_line | host all all 127.0.0.1/32 trust

I'm wondering for instance if it would be better to (optionally?) keep
iterating over the lines and print all lines that would have matched and in
which order, or if the function should return the parsed line information
rather than the raw line, although it could make the output worse if using huge
secondary auth files. I'm also not sure if the various possible flags (ssl,
gss) should be explicitly set or if the function should try various
permutations on its own.

Note that this function only works against the current configuration files, not
the one currently active. As far as I can see PostgresMain gets rid of
PostmasterContext, which is where they currently live, so they don't exist
anymore in the backends. To make it work we would have to always keep those in
each backend, but also reload them along with the main config file on SIGHUP.
Except on Windows (and -DEXEC_BACKEND), as the current version of auth files
are always used anyway. So would it make sense to add the possibility to use
the loaded files instead, given the required extra cost and the fact that it
wouldn't make sense on Windows? If yes, I guess that we should also allow that
in the pg_hba / pg_ident views.

While at it, I noticed this comment added in de16ab72388:

The <filename>pg_hba.conf</filename> file is read on start-up and when
the main server process receives a
<systemitem>SIGHUP</systemitem><indexterm><primary>SIGHUP</primary></indexterm>
signal. If you edit the file on an
active system, you will need to signal the postmaster
(using <literal>pg_ctl reload</literal>, calling the SQL function
<function>pg_reload_conf()</function>, or using <literal>kill
-HUP</literal>) to make it re-read the file.
[...]
The preceding statement is not true on Microsoft Windows: there, any
changes in the <filename>pg_hba.conf</filename> file are immediately
applied by subsequent new connections.

I also find this comment a bit misleading. Wouldn't it be better to have a
similar statement for pg_ident, or at least a note that it applies to both
files?

Attachments:

v1-0001-Add-a-pg_ident_file_mappings-view.patchtext/plain; charset=us-asciiDownload
From 61437776c103b7a3520ab6486cc5f1e88df0f80a Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 17:38:34 +0800
Subject: [PATCH v1 1/3] Add a pg_ident_file_mappings view.

This view is similar to pg_hba_file_rules view, and can be also helpful to help
diagnosing configuration problems.

A following commit will add the possibility to include files in pg_hba and
pg_ident configuration files, which will then make this view even more useful.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: FIXME
---
 doc/src/sgml/catalogs.sgml             | 108 ++++++++++++++
 doc/src/sgml/client-auth.sgml          |  10 ++
 doc/src/sgml/func.sgml                 |   5 +-
 src/backend/catalog/system_views.sql   |   6 +
 src/backend/libpq/hba.c                | 191 +++++++++++++++++++++++--
 src/include/catalog/pg_proc.dat        |   7 +
 src/test/regress/expected/rules.out    |   6 +
 src/test/regress/expected/sysviews.out |   7 +
 src/test/regress/sql/sysviews.sql      |   3 +
 9 files changed, 328 insertions(+), 15 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 83987a9904..0de40a9626 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9530,6 +9530,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <entry>summary of client authentication configuration file contents</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link></entry>
+      <entry>summary of client user name mapping configuration file contents</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-indexes"><structname>pg_indexes</structname></link></entry>
       <entry>indexes</entry>
@@ -10523,6 +10528,109 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </para>
  </sect1>
 
+ <sect1 id="view-pg-ident-file-mappings">
+  <title><structname>pg_ident_file_mappings</structname></title>
+
+  <indexterm zone="view-pg-ident-file-mappings">
+   <primary>pg_ident_file_mappings</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_ident_file_mappings</structname> provides a summary
+   of the contents of the client user name mapping configuration file,
+   <link linkend="auth-username-maps"><filename>pg_ident.conf</filename></link>.
+   A row appears in this view for each
+   non-empty, non-comment line in the file, with annotations indicating
+   whether the rule could be applied successfully.
+  </para>
+
+  <para>
+   This view can be helpful for checking whether planned changes in the
+   authentication configuration file will work, or for diagnosing a previous
+   failure.  Note that this view reports on the <emphasis>current</emphasis>
+   contents of the file, not on what was last loaded by the server.
+  </para>
+
+  <para>
+   By default, the <structname>pg_ident_file_mappings</structname> view can be
+   read only by superusers.
+  </para>
+
+  <table>
+   <title><structname>pg_ident_file_mappings</structname> Columns</title> <tgroup
+   cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this rule in <filename>pg_ident.conf</filename>
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>map_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the map
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>sys_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Detected user name of the client
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pg_username</structfield> <type>text</type>
+      </para>
+      <para>
+       Requested PostgreSQL user name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>error</structfield> <type>text</type>
+      </para>
+      <para>
+       If not null, an error message indicating why this line could not be
+       processed
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+  <para>
+   Usually, a row reflecting an incorrect entry will have values for only
+   the <structfield>line_number</structfield> and <structfield>error</structfield> fields.
+  </para>
+
+  <para>
+   See <xref linkend="client-authentication"/> for more information about
+   client authentication configuration.
+  </para>
+ </sect1>
+
  <sect1 id="view-pg-indexes">
   <title><structname>pg_indexes</structname></title>
 
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..142b0affcb 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -896,6 +896,16 @@ mymap   /^(.*)@otherdomain\.com$   guest
    -HUP</literal>) to make it re-read the file.
   </para>
 
+  <para>
+   The system view
+   <link linkend="view-pg-ident-file-mappings"><structname>pg_ident_file_mappings</structname></link>
+   can be helpful for pre-testing changes to the
+   <filename>pg_ident.conf</filename> file, or for diagnosing problems if
+   loading of the file did not have the desired effects.  Rows in the view with
+   non-null <structfield>error</structfield> fields indicate problems in the
+   corresponding lines of the file.
+  </para>
+
   <para>
    A <filename>pg_ident.conf</filename> file that could be used in
    conjunction with the <filename>pg_hba.conf</filename> file in <xref
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index df3cd5987b..bbac492043 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25475,8 +25475,9 @@ SELECT collation for ('foo' COLLATE "de_DE");
         sending a <systemitem>SIGHUP</systemitem> signal to the postmaster
         process, which in turn sends <systemitem>SIGHUP</systemitem> to each
         of its children.) You can use the
-        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link> and
-        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> views
+        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link>,
+        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> and
+        <link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link> views
         to check the configuration files for possible errors, before reloading.
        </para></entry>
       </row>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 3cb69b1f87..ef13f470b3 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -607,6 +607,12 @@ CREATE VIEW pg_hba_file_rules AS
 REVOKE ALL ON pg_hba_file_rules FROM PUBLIC;
 REVOKE EXECUTE ON FUNCTION pg_hba_file_rules() FROM PUBLIC;
 
+CREATE VIEW pg_ident_file_mappings AS
+   SELECT * FROM pg_ident_file_mappings() AS A;
+
+REVOKE ALL ON pg_ident_file_mappings FROM PUBLIC;
+REVOKE EXECUTE ON FUNCTION pg_ident_file_mappings() FROM PUBLIC;
+
 CREATE VIEW pg_timezone_abbrevs AS
     SELECT * FROM pg_timezone_abbrevs();
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index d84a40b726..9a9ce96fee 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -148,6 +148,9 @@ static ArrayType *gethba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 						  int lineno, HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int lineno, IdentLine *ident, const char *err_msg);
+static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
 /*
@@ -914,25 +917,22 @@ do { \
 } while (0)
 
 /*
- * Macros for handling pg_ident problems.
- * Much as above, but currently the message level is hardwired as LOG
- * and there is no provision for an err_msg string.
+ * Macros for handling pg_ident problems, similar as above.
  *
  * IDENT_FIELD_ABSENT:
- * Log a message and exit the function if the given ident field ListCell is
- * not populated.
+ * Reports when the given ident field ListCell is not populated.
  *
  * IDENT_MULTI_VALUE:
- * Log a message and exit the function if the given ident token List has more
- * than one element.
+ * Reports if the given ident token List has more than one element.
  */
 #define IDENT_FIELD_ABSENT(field) \
 do { \
 	if (!field) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
 						IdentFileName, line_num))); \
+		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
 } while (0)
@@ -940,11 +940,12 @@ do { \
 #define IDENT_MULTI_VALUE(tokens) \
 do { \
 	if (tokens->length > 1) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
 							line_num, IdentFileName))); \
+		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
 } while (0)
@@ -2753,7 +2754,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
  *
- * If parsing fails, log a message and return NULL.
+ * If parsing fails, log a message at ereport level elevel, store an error
+ * string in tok_line->err_msg and return NULL.
  *
  * If ident_user is a regular expression (ie. begins with a slash), it is
  * compiled and stored in IdentLine structure.
@@ -2763,9 +2765,10 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * NULL.
  */
 static IdentLine *
-parse_ident_line(TokenizedLine *tok_line)
+parse_ident_line(TokenizedLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
 	List	   *tokens;
 	HbaToken   *token;
@@ -2819,11 +2822,14 @@ parse_ident_line(TokenizedLine *tok_line)
 			char		errstr[100];
 
 			pg_regerror(r, &parsedline->re, errstr, sizeof(errstr));
-			ereport(LOG,
+			ereport(elevel,
 					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
 					 errmsg("invalid regular expression \"%s\": %s",
 							parsedline->ident_user + 1, errstr)));
 
+			*err_msg = psprintf("invalid regular expression \"%s\": %s",
+							   parsedline->ident_user + 1, errstr);
+
 			pfree(wstr);
 			return NULL;
 		}
@@ -3074,7 +3080,7 @@ load_ident(void)
 			continue;
 		}
 
-		if ((newline = parse_ident_line(tok_line)) == NULL)
+		if ((newline = parse_ident_line(tok_line, LOG)) == NULL)
 		{
 			/* Parse error; remember there's trouble */
 			ok = false;
@@ -3164,3 +3170,162 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+
+/*
+ * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
+ * tuplestore
+ *
+ * tuple_store: where to store data
+ * tupdesc: tuple descriptor for the view
+ * lineno: pg_hba.conf line number (must always be valid)
+ * ident: parsed line data (can be NULL, in which case err_msg should be set)
+ * err_msg: error message (NULL if none)
+ *
+ * Note: leaks memory, but we don't care since this is run in a short-lived
+ * memory context.
+ */
+static void
+fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+				int lineno, IdentLine *ident, const char *err_msg)
+{
+	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	HeapTuple	tuple;
+	int			index;
+
+	Assert(tupdesc->natts == NUM_PG_IDENT_FILE_MAPPINGS_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (ident != NULL)
+	{
+		values[index++] = CStringGetTextDatum(ident->usermap);
+		values[index++] = CStringGetTextDatum(ident->ident_user);
+		values[index++] = CStringGetTextDatum(ident->pg_role);
+	}
+	else
+	{
+		/* no parsing result, so set relevant fields to nulls */
+		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * Read the pg_ident.conf file and fill the tuplestore with view records.
+ */
+static void
+fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *ident_lines = NIL;
+	ListCell   *line;
+	MemoryContext linecxt;
+	MemoryContext hbacxt;
+	MemoryContext oldcxt;
+
+	/*
+	 * In the unlikely event that we can't open pg_hba.conf, we throw an
+	 * error, rather than trying to report it via some sort of view entry.
+	 * (Most other error conditions should result in a message in a view
+	 * entry.)
+	 */
+	file = AllocateFile(IdentFileName, "r");
+	if (file == NULL)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open usermap file \"%s\": %m",
+						IdentFileName)));
+
+	linecxt = tokenize_file(HbaFileName, file, &ident_lines, DEBUG3);
+	FreeFile(file);
+
+	/* Now parse all the lines */
+	hbacxt = AllocSetContextCreate(CurrentMemoryContext,
+								   "ident parser context",
+								   ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hbacxt);
+	foreach(line, ident_lines)
+	{
+		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line);
+		IdentLine   *identline = NULL;
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg == NULL)
+			identline = parse_ident_line(tok_line, DEBUG3);
+
+		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
+					  tok_line->err_msg);
+	}
+
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	MemoryContextSwitchTo(oldcxt);
+}
+
+/*
+ * SQL-accessible SRF to return all the entries in the pg_ident.conf file.
+ */
+Datum
+pg_ident_file_mappings(PG_FUNCTION_ARGS)
+{
+	Tuplestorestate *tuple_store;
+	TupleDesc	tupdesc;
+	MemoryContext old_cxt;
+	ReturnSetInfo *rsi;
+
+	/*
+	 * We must use the Materialize mode to be safe against ident file changes
+	 * while the cursor is open. It's also more efficient than having to look
+	 * up our current position in the parsed list every time.
+	 */
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	/* Check to see if caller supports us returning a tuplestore */
+	if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsi->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	rsi->returnMode = SFRM_Materialize;
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Build tuplestore to hold the result rows */
+	old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
+
+	tuple_store =
+		tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random,
+							  false, work_mem);
+	rsi->setDesc = tupdesc;
+	rsi->setResult = tuple_store;
+
+	MemoryContextSwitchTo(old_cxt);
+
+	/* Fill the tuplestore */
+	fill_ident_view(tuple_store, tupdesc);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7f1ee97f55..2c8f5d9c13 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6121,6 +6121,13 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o}',
   proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
+{ oid => '9556', descr => 'show pg_ident.conf mappings',
+  proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
+  provolatile => 'v', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o}',
+  proargnames => '{line_number,map_name,sys_name,pg_usernamee,error}',
+  prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 1420288d67..62cf0d8674 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1347,6 +1347,12 @@ pg_hba_file_rules| SELECT a.line_number,
     a.options,
     a.error
    FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.line_number,
+    a.map_name,
+    a.sys_name,
+    a.pg_usernamee,
+    a.error
+   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_usernamee, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 442eeb1e3f..d8a7df9498 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -55,6 +55,13 @@ select count(*) > 0 as ok from pg_hba_file_rules;
  t
 (1 row)
 
+-- We expect no user mapping in this test
+select count(*) = 0 as ok from pg_ident_file_mappings;
+ ok 
+----
+ t
+(1 row)
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
  ok 
diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql
index 4980f07be2..4c1129e787 100644
--- a/src/test/regress/sql/sysviews.sql
+++ b/src/test/regress/sql/sysviews.sql
@@ -28,6 +28,9 @@ select count(*) >= 0 as ok from pg_file_settings;
 -- There will surely be at least one rule
 select count(*) > 0 as ok from pg_hba_file_rules;
 
+-- We expect no user mapping in this test
+select count(*) = 0 as ok from pg_ident_file_mappings;
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
 
-- 
2.33.1

v1-0002-Allow-file-inclusion-in-pg_hba-and-pg_ident-files.patchtext/plain; charset=us-asciiDownload
From fc24bf043a2792930a61dccfd82f94dd5ca23bee Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 15:45:26 +0800
Subject: [PATCH v1 2/3] Allow file inclusion in pg_hba and pg_ident files.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: FIXME
---
 doc/src/sgml/catalogs.sgml             |  48 ++++-
 doc/src/sgml/client-auth.sgml          |  34 +++-
 src/backend/libpq/hba.c                | 252 +++++++++++++++++++------
 src/backend/libpq/pg_hba.conf.sample   |   8 +-
 src/backend/libpq/pg_ident.conf.sample |   8 +-
 src/include/catalog/pg_proc.dat        |  12 +-
 src/include/libpq/hba.h                |   1 +
 src/test/regress/expected/rules.out    |  12 +-
 8 files changed, 304 insertions(+), 71 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 0de40a9626..07c6679a52 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -10430,12 +10430,31 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule in the given file_name
       </para></entry>
      </row>
 
@@ -10571,6 +10590,33 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this mapping if the mapping is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this mapping
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this mapping in the given file_name
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 142b0affcb..e1d0e103b3 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,17 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,6 +112,7 @@
   <para>
    A record can have several formats:
 <synopsis>
+include       <replaceable>file</replaceable>
 local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
 host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
@@ -118,6 +128,15 @@ hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceabl
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -835,8 +854,9 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -847,6 +867,14 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   The lines can record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9a9ce96fee..8b72141342 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -89,11 +89,18 @@ typedef struct HbaToken
 typedef struct TokenizedLine
 {
 	List	   *fields;			/* List of lists of HbaTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
 } TokenizedLine;
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -140,17 +147,25 @@ static const char *const UserAuthName[] =
 
 static MemoryContext tokenize_file(const char *filename, FILE *file,
 								   List **tok_lines, int elevel);
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static ArrayType *gethba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int mapping_number, const char *filename,
 							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
 
 
 /*
@@ -390,36 +405,11 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, outer_filename,
+							 elevel, err_msg, &inc_fullname);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
 	linecxt = tokenize_file(inc_fullname, inc_file, &inc_lines, elevel);
@@ -458,11 +448,37 @@ tokenize_inc_file(List *tokens,
 	return tokens;
 }
 
+/*
+ * Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a decicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+static MemoryContext
+tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, elevel);
+
+	return linecxt;
+}
+
 /*
  * Tokenize the given file.
  *
  * The output is a list of TokenizedLine structs; see struct definition above.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -471,27 +487,19 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-static MemoryContext
-tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
 		char	   *lineptr;
@@ -553,28 +561,76 @@ tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
 				current_line = lappend(current_line, current_field);
 		}
 
-		/* Reached EOL; emit line to TokenizedLine list unless it's boring */
-		if (current_line != NIL || err_msg != NULL)
+		/*
+		 * Reached EOL; no need to emit line to TokenizedLine list if it's
+		 * boring
+		 */
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			HbaToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+				char	   *inc_fullname;
+				FILE	   *inc_file;
+
+				inc_filename = second->string;
+
+				inc_file = open_inc_file(IncludedAuthFile, inc_filename,
+										 filename, elevel, &err_msg,
+										 &inc_fullname);
+
+				/*
+				 * We could open the file, recursively process it. Errors will
+				 * be reported in the general TokenizedLine processing.
+				 */
+				if (inc_file != NULL)
+				{
+					tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+											   tok_lines, elevel);
+
+					FreeFile(inc_file);
+					pfree(inc_fullname);
+
+					goto next_line;
+				}
+				else
+				{
+					/* We should got an error */
+					Assert(err_msg != NULL);
+				}
+			}
+		}
+
+		/* Emit line to TokenizedLine */
 		{
 			TokenizedLine *tok_line;
 
 			tok_line = (TokenizedLine *) palloc(sizeof(TokenizedLine));
 			tok_line->fields = current_line;
+			tok_line->file_name = pstrdup(filename);
 			tok_line->line_num = line_number;
 			tok_line->raw_line = pstrdup(buf.data);
 			tok_line->err_msg = err_msg;
 			*tok_lines = lappend(*tok_lines, tok_line);
 		}
 
+next_line:
 		line_number += continuations + 1;
+
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -981,6 +1037,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(tok_line->file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -2453,7 +2510,7 @@ gethba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line: build one row of pg_hba_file_rules view, add it to tuplestore
@@ -2469,7 +2526,8 @@ gethba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -2488,6 +2546,13 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -2631,7 +2696,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -2653,6 +2718,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -2687,8 +2753,12 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -2698,6 +2768,64 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	MemoryContextDelete(hbacxt);
 }
 
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		switch (kind)
+		{
+			case SecondaryAuthFile:
+				msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+				msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+				break;
+			case IncludedAuthFile:
+				msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+				msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+				break;
+			default:
+				elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+				break;
+		}
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg(msglog, inc_filename, *inc_fullname)));
+		*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+							strerror(save_errno));
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
 /*
  * SQL-accessible SRF to return all the entries in the pg_hba.conf file.
  */
@@ -3172,7 +3300,7 @@ hba_authname(UserAuth auth_method)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -3189,7 +3317,8 @@ hba_authname(UserAuth auth_method)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -3202,6 +3331,13 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -3214,7 +3350,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -3236,6 +3372,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -3270,8 +3407,13 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-					  tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..0b6589a7b9 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,6 +9,7 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
+# include       FILE
 # local         DATABASE  USER  METHOD  [OPTIONS]
 # host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 # hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
@@ -18,7 +19,12 @@
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", it's not a mapping record but a directive to
+# include records from another file, specified in the field.  FILE is the file
+# to include.  It can be specified with a relative or absolute path, and can be
+# double quoted if it contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..138359cf03 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,18 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
+# include  FILE
 # MAPNAME  SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", it's not an authentication record but a
+# directive to include records from another file, specified in the field.  FILE
+# is the file to include.  It can be specified with a relative or absolute
+# path, and can be double quoted if it contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2c8f5d9c13..2292115c85 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6117,16 +6117,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '9556', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_usernamee,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_usernamee,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8d9f3821b1..45d6ce1f22 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 62cf0d8674..e6f274cb59 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,9 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1348,15 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_usernamee,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_usernamee, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_usernamee, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.33.1

v1-0003-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From eba83c6e02a565fbbc66779464de3ca99e9935c8 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v1 3/3] POC: Add a pg_hba_matches() function.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: FIXME
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 127 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 143 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index fd1421788e..ae839a4b76 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 8b72141342..42fcf9edca 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -41,6 +41,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -3471,3 +3472,129 @@ pg_ident_file_mappings(PG_FUNCTION_ARGS)
 
 	PG_RETURN_NULL();
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+
+		/* See pg_inet_net_ntop() for details about those constants */
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+				inet_pton(AF_INET, tmp, &dst->sin_addr);
+				break;
+			}
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+				inet_pton(AF_INET6, tmp, &dst->sin6_addr);
+				break;
+			}
+#endif
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2292115c85..b33cad2848 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6128,6 +6128,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_usernamee,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.33.1

#2Nathan Bossart
nathandbossart@gmail.com
In reply to: Julien Rouhaud (#1)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Feb 23, 2022 at 12:59:59PM +0800, Julien Rouhaud wrote:

To address that, I'd like to propose the possibility to include files in hba
and ident configuration files. This was already discussed in the past, and in
my understanding this is mostly wanted, while some people expressed concerned
on a use case that wouldn't rely on thousands of entries.

+1, I think this would be very useful.

0001 adds a new pg_ident_file_mappings view, which is basically the same as
pg_hba_file_rules view but for mappings. It's probably already useful, for
instance if you need to tweak some regexp.

This seems reasonable.

Finally I also added 0003, which is a POC for a new pg_hba_matches() function,
that can help DBA to understand why their configuration isn't working as they
expect. This only to start the discussion on that topic, the code is for now
really hackish, as I don't know how much this is wanted and/or if some other
behavior would be better, and there's also no documentation or test. The
function for now only takes an optional inet (null means unix socket), the
target role and an optional ssl flag and returns the file, line and raw line
matching if any, or null. For instance:

I think another use-case for this is testing updates to your configuration
files. For example, I could ensure that hba_forbid_non_ssl.conf wasn't
accidentally reverted as part of an unrelated change.

--
Nathan Bossart
Amazon Web Services: https://aws.amazon.com

#3Michael Paquier
michael@paquier.xyz
In reply to: Nathan Bossart (#2)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Feb 23, 2022 at 09:44:58AM -0800, Nathan Bossart wrote:

On Wed, Feb 23, 2022 at 12:59:59PM +0800, Julien Rouhaud wrote:

0001 adds a new pg_ident_file_mappings view, which is basically the same as
pg_hba_file_rules view but for mappings. It's probably already useful, for
instance if you need to tweak some regexp.

This seems reasonable.

Interesting. One can note that hba.c is already large, and this makes
the file larger. I'd like to think that it would be better to move
all the code related to the SQL functions for pg_hba.conf and such to
a new hbafuncs.c under adt/. Would that make sense?
--
Michael

#4Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#3)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Sat, Feb 26, 2022 at 03:04:43PM +0900, Michael Paquier wrote:

On Wed, Feb 23, 2022 at 09:44:58AM -0800, Nathan Bossart wrote:

On Wed, Feb 23, 2022 at 12:59:59PM +0800, Julien Rouhaud wrote:

0001 adds a new pg_ident_file_mappings view, which is basically the same as
pg_hba_file_rules view but for mappings. It's probably already useful, for
instance if you need to tweak some regexp.

This seems reasonable.

Interesting. One can note that hba.c is already large, and this makes
the file larger. I'd like to think that it would be better to move
all the code related to the SQL functions for pg_hba.conf and such to
a new hbafuncs.c under adt/. Would that make sense?

I'm fine with it. Assuming that you meant to move also the underlying
functions that goes with it (fill_hba_line and such), that would end up
removing about 15% of hba.c (after applying 0001, 0002 and 0003).

Note that in order to do so we would need to expose quite a lot more about hba
internals, like tokenize_file() and parse_hba_line(), along with structs
HbaToken and TokenizedLine.

#5Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#4)
Re: Allow file inclusion in pg_hba and pg_ident files

On Sat, Feb 26, 2022 at 02:27:15PM +0800, Julien Rouhaud wrote:

I'm fine with it. Assuming that you meant to move also the underlying
functions that goes with it (fill_hba_line and such), that would end up
removing about 15% of hba.c (after applying 0001, 0002 and 0003).

Cool. Thanks for the number.

Note that in order to do so we would need to expose quite a lot more about hba
internals, like tokenize_file() and parse_hba_line(), along with structs
HbaToken and TokenizedLine.

I'd be rather fine with exposing all that in the shape of a clean
split, renaming some of those structures and/or function with an
Hba-like prefix, for consistency.
--
Michael

#6Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#5)
Re: Allow file inclusion in pg_hba and pg_ident files

On Sat, Feb 26, 2022 at 03:36:19PM +0900, Michael Paquier wrote:

On Sat, Feb 26, 2022 at 02:27:15PM +0800, Julien Rouhaud wrote:

Note that in order to do so we would need to expose quite a lot more about hba
internals, like tokenize_file() and parse_hba_line(), along with structs
HbaToken and TokenizedLine.

I'd be rather fine with exposing all that in the shape of a clean
split, renaming some of those structures and/or function with an
Hba-like prefix, for consistency.

Of course. I was thinking using "auth" for something that's common to pg_hba
and pg_ident (like e.g. TokenizeAuthFile()), and otherwise keep the current
hba/ident prefix.

Unless someone object or suggest better naming in the next few days I will take
care of that.

I would also welcome some opinion on the points I mentioned about 0002.

#7Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#6)
Re: Allow file inclusion in pg_hba and pg_ident files

On Sat, Feb 26, 2022 at 02:50:33PM +0800, Julien Rouhaud wrote:

Of course. I was thinking using "auth" for something that's common to pg_hba
and pg_ident (like e.g. TokenizeAuthFile()), and otherwise keep the current
hba/ident prefix.

Okay, thanks.

Unless someone object or suggest better naming in the next few days I will take
care of that.

I don't have an opinion to share about 0002 and 0003 yet, but 0001
seems like a good idea on its own.
--
Michael

#8Julien Rouhaud
rjuju123@gmail.com
In reply to: Nathan Bossart (#2)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Wed, Feb 23, 2022 at 09:44:58AM -0800, Nathan Bossart wrote:

Finally I also added 0003, which is a POC for a new pg_hba_matches() function,
that can help DBA to understand why their configuration isn't working as they
expect. This only to start the discussion on that topic, the code is for now
really hackish, as I don't know how much this is wanted and/or if some other
behavior would be better, and there's also no documentation or test. The
function for now only takes an optional inet (null means unix socket), the
target role and an optional ssl flag and returns the file, line and raw line
matching if any, or null. For instance:

I think another use-case for this is testing updates to your configuration
files. For example, I could ensure that hba_forbid_non_ssl.conf wasn't
accidentally reverted as part of an unrelated change.

Indeed, that function could really be helpful in many scenario. Note that this
isn't my idea but Magnus idea, which he mentioned quite a long time ago.

#9Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#7)
4 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Mon, Feb 28, 2022 at 05:01:07PM +0900, Michael Paquier wrote:

On Sat, Feb 26, 2022 at 02:50:33PM +0800, Julien Rouhaud wrote:

Of course. I was thinking using "auth" for something that's common to pg_hba
and pg_ident (like e.g. TokenizeAuthFile()), and otherwise keep the current
hba/ident prefix.

Okay, thanks.

Done in attached v2. I did the split in a separate commit, as the diff is
otherwise unreadable. While at it I also fixed a few minor issues (I missed a
MemoryContextDelete, and now avoid relying on inet_net_pton which apparently
doesn't exist in cygwin).

Attachments:

v2-0001-Extract-view-processing-code-from-hba.c.patchtext/plain; charset=us-asciiDownload
From c2b5168608e34acb171e0da6f4e9e16c14f82adb Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 28 Feb 2022 16:50:00 +0800
Subject: [PATCH v2 1/4] Extract view processing code from hba.c

This file is already quite big and a later commit will add an additional view,
so let's move all the view related code in hba.c into a new adt/hbafuncs.c.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/libpq/hba.c          | 3618 +++++++++++++-----------------
 src/backend/utils/adt/Makefile   |    1 +
 src/backend/utils/adt/hbafuncs.c |  452 ++++
 src/include/libpq/hba.h          |   29 +
 src/tools/pgindent/typedefs.list |    2 +-
 5 files changed, 2067 insertions(+), 2035 deletions(-)
 create mode 100644 src/backend/utils/adt/hbafuncs.c

diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index d84a40b726..184df4942a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -68,32 +68,6 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
-/*
- * A single string token lexed from a config file, together with whether
- * the token had been quoted.
- */
-typedef struct HbaToken
-{
-	char	   *string;
-	bool		quoted;
-} HbaToken;
-
-/*
- * TokenizedLine represents one line lexed from a config file.
- * Each item in the "fields" list is a sub-list of HbaTokens.
- * We don't emit a TokenizedLine for empty or all-comment lines,
- * so "fields" is never NIL (nor are any of its sub-lists).
- * Exception: if an error occurs during tokenization, we might
- * have fields == NIL, in which case err_msg != NULL.
- */
-typedef struct TokenizedLine
-{
-	List	   *fields;			/* List of lists of HbaTokens */
-	int			line_num;		/* Line number */
-	char	   *raw_line;		/* Raw line text */
-	char	   *err_msg;		/* Error message if any */
-} TokenizedLine;
-
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -138,16 +112,10 @@ static const char *const UserAuthName[] =
 };
 
 
-static MemoryContext tokenize_file(const char *filename, FILE *file,
-								   List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
-static ArrayType *gethba_options(HbaLine *hba);
-static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
-static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
 /*
@@ -419,7 +387,7 @@ tokenize_inc_file(List *tokens,
 	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -427,7 +395,7 @@ tokenize_inc_file(List *tokens,
 	/* Copy all tokens found in the file and append to the tokens list */
 	foreach(inc_line, inc_lines)
 	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(inc_line);
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(inc_line);
 		ListCell   *inc_field;
 
 		/* If any line has an error, propagate that up to caller */
@@ -455,122 +423,6 @@ tokenize_inc_file(List *tokens,
 	return tokens;
 }
 
-/*
- * Tokenize the given file.
- *
- * The output is a list of TokenizedLine structs; see struct definition above.
- *
- * filename: the absolute path to the target file
- * file: the already-opened target file
- * tok_lines: receives output list
- * elevel: message logging level
- *
- * Errors are reported by logging messages at ereport level elevel and by
- * adding TokenizedLine structs containing non-null err_msg fields to the
- * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
- */
-static MemoryContext
-tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
-{
-	int			line_number = 1;
-	StringInfoData buf;
-	MemoryContext linecxt;
-	MemoryContext oldcxt;
-
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_file",
-									ALLOCSET_SMALL_SIZES);
-	oldcxt = MemoryContextSwitchTo(linecxt);
-
-	initStringInfo(&buf);
-
-	*tok_lines = NIL;
-
-	while (!feof(file) && !ferror(file))
-	{
-		char	   *lineptr;
-		List	   *current_line = NIL;
-		char	   *err_msg = NULL;
-		int			last_backslash_buflen = 0;
-		int			continuations = 0;
-
-		/* Collect the next input line, handling backslash continuations */
-		resetStringInfo(&buf);
-
-		while (pg_get_line_append(file, &buf, NULL))
-		{
-			/* Strip trailing newline, including \r in case we're on Windows */
-			buf.len = pg_strip_crlf(buf.data);
-
-			/*
-			 * Check for backslash continuation.  The backslash must be after
-			 * the last place we found a continuation, else two backslashes
-			 * followed by two \n's would behave surprisingly.
-			 */
-			if (buf.len > last_backslash_buflen &&
-				buf.data[buf.len - 1] == '\\')
-			{
-				/* Continuation, so strip it and keep reading */
-				buf.data[--buf.len] = '\0';
-				last_backslash_buflen = buf.len;
-				continuations++;
-				continue;
-			}
-
-			/* Nope, so we have the whole line */
-			break;
-		}
-
-		if (ferror(file))
-		{
-			/* I/O error! */
-			int			save_errno = errno;
-
-			ereport(elevel,
-					(errcode_for_file_access(),
-					 errmsg("could not read file \"%s\": %m", filename)));
-			err_msg = psprintf("could not read file \"%s\": %s",
-							   filename, strerror(save_errno));
-			break;
-		}
-
-		/* Parse fields */
-		lineptr = buf.data;
-		while (*lineptr && err_msg == NULL)
-		{
-			List	   *current_field;
-
-			current_field = next_field_expand(filename, &lineptr,
-											  elevel, &err_msg);
-			/* add field to line, unless we are at EOL or comment start */
-			if (current_field != NIL)
-				current_line = lappend(current_line, current_field);
-		}
-
-		/* Reached EOL; emit line to TokenizedLine list unless it's boring */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedLine *tok_line;
-
-			tok_line = (TokenizedLine *) palloc(sizeof(TokenizedLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
-		}
-
-		line_number += continuations + 1;
-	}
-
-	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
-}
-
 
 /*
  * Does user belong to role?
@@ -950,2217 +802,1915 @@ do { \
 } while (0)
 
 
+
 /*
- * Parse one tokenised line from the hba config file and store the result in a
- * HbaLine structure.
- *
- * If parsing fails, log a message at ereport level elevel, store an error
- * string in tok_line->err_msg, and return NULL.  (Some non-error conditions
- * can also result in such messages.)
- *
- * Note: this function leaks memory when an error occurs.  Caller is expected
- * to have set a memory context that will be reset if this function returns
- * NULL.
+ * Parse one name-value pair as an authentication option into the given
+ * HbaLine.  Return true if we successfully parse the option, false if we
+ * encounter an error.  In the event of an error, also log a message at
+ * ereport level elevel, and store a message string into *err_msg.
  */
-static HbaLine *
-parse_hba_line(TokenizedLine *tok_line, int elevel)
+static bool
+parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
+				   int elevel, char **err_msg)
 {
-	int			line_num = tok_line->line_num;
-	char	  **err_msg = &tok_line->err_msg;
-	char	   *str;
-	struct addrinfo *gai_result;
-	struct addrinfo hints;
-	int			ret;
-	char	   *cidr_slash;
-	char	   *unsupauth;
-	ListCell   *field;
-	List	   *tokens;
-	ListCell   *tokencell;
-	HbaToken   *token;
-	HbaLine    *parsedline;
+	int			line_num = hbaline->linenumber;
 
-	parsedline = palloc0(sizeof(HbaLine));
-	parsedline->linenumber = line_num;
-	parsedline->rawline = pstrdup(tok_line->raw_line);
+#ifdef USE_LDAP
+	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
+#endif
 
-	/* Check the record type. */
-	Assert(tok_line->fields != NIL);
-	field = list_head(tok_line->fields);
-	tokens = lfirst(field);
-	if (tokens->length > 1)
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("multiple values specified for connection type"),
-				 errhint("Specify exactly one connection type per line."),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = "multiple values specified for connection type";
-		return NULL;
-	}
-	token = linitial(tokens);
-	if (strcmp(token->string, "local") == 0)
+	if (strcmp(name, "map") == 0)
 	{
-#ifdef HAVE_UNIX_SOCKETS
-		parsedline->conntype = ctLocal;
-#else
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("local connections are not supported by this build"),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = "local connections are not supported by this build";
-		return NULL;
-#endif
+		if (hbaline->auth_method != uaIdent &&
+			hbaline->auth_method != uaPeer &&
+			hbaline->auth_method != uaGSS &&
+			hbaline->auth_method != uaSSPI &&
+			hbaline->auth_method != uaCert)
+			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, and cert"));
+		hbaline->usermap = pstrdup(val);
 	}
-	else if (strcmp(token->string, "host") == 0 ||
-			 strcmp(token->string, "hostssl") == 0 ||
-			 strcmp(token->string, "hostnossl") == 0 ||
-			 strcmp(token->string, "hostgssenc") == 0 ||
-			 strcmp(token->string, "hostnogssenc") == 0)
+	else if (strcmp(name, "clientcert") == 0)
 	{
+		if (hbaline->conntype != ctHostSSL)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "clientcert can only be configured for \"hostssl\" rows";
+			return false;
+		}
 
-		if (token->string[4] == 's')	/* "hostssl" */
+		if (strcmp(val, "verify-full") == 0)
 		{
-			parsedline->conntype = ctHostSSL;
-			/* Log a warning if SSL support is not active */
-#ifdef USE_SSL
-			if (!EnableSSL)
+			hbaline->clientcert = clientCertFull;
+		}
+		else if (strcmp(val, "verify-ca") == 0)
+		{
+			if (hbaline->auth_method == uaCert)
 			{
 				ereport(elevel,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
-						 errmsg("hostssl record cannot match because SSL is disabled"),
-						 errhint("Set ssl = on in postgresql.conf."),
+						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
 						 errcontext("line %d of configuration file \"%s\"",
 									line_num, HbaFileName)));
-				*err_msg = "hostssl record cannot match because SSL is disabled";
+				*err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
+				return false;
 			}
-#else
+
+			hbaline->clientcert = clientCertCA;
+		}
+		else
+		{
 			ereport(elevel,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
-					 errhint("Compile with --with-ssl to use SSL connections."),
+					 errmsg("invalid value for clientcert: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
 								line_num, HbaFileName)));
-			*err_msg = "hostssl record cannot match because SSL is not supported by this build";
-#endif
+			return false;
 		}
-		else if (token->string[4] == 'g')	/* "hostgssenc" */
+	}
+	else if (strcmp(name, "clientname") == 0)
+	{
+		if (hbaline->conntype != ctHostSSL)
 		{
-			parsedline->conntype = ctHostGSS;
-#ifndef ENABLE_GSS
 			ereport(elevel,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
-					 errhint("Compile with --with-gssapi to use GSSAPI connections."),
+					 errmsg("clientname can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
 								line_num, HbaFileName)));
-			*err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
-#endif
+			*err_msg = "clientname can only be configured for \"hostssl\" rows";
+			return false;
 		}
-		else if (token->string[4] == 'n' && token->string[6] == 's')
-			parsedline->conntype = ctHostNoSSL;
-		else if (token->string[4] == 'n' && token->string[6] == 'g')
-			parsedline->conntype = ctHostNoGSS;
-		else
+
+		if (strcmp(val, "CN") == 0)
 		{
-			/* "host" */
-			parsedline->conntype = ctHost;
+			hbaline->clientcertname = clientCertCN;
 		}
-	}							/* record type */
-	else
+		else if (strcmp(val, "DN") == 0)
+		{
+			hbaline->clientcertname = clientCertDN;
+		}
+		else
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("invalid value for clientname: \"%s\"", val),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			return false;
+		}
+	}
+	else if (strcmp(name, "pamservice") == 0)
 	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("invalid connection type \"%s\"",
-						token->string),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = psprintf("invalid connection type \"%s\"", token->string);
-		return NULL;
+		REQUIRE_AUTH_OPTION(uaPAM, "pamservice", "pam");
+		hbaline->pamservice = pstrdup(val);
 	}
+	else if (strcmp(name, "pam_use_hostname") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaPAM, "pam_use_hostname", "pam");
+		if (strcmp(val, "1") == 0)
+			hbaline->pam_use_hostname = true;
+		else
+			hbaline->pam_use_hostname = false;
 
-	/* Get the databases. */
-	field = lnext(tok_line->fields, field);
-	if (!field)
+	}
+	else if (strcmp(name, "ldapurl") == 0)
 	{
+#ifdef LDAP_API_FEATURE_X_OPENLDAP
+		LDAPURLDesc *urldata;
+		int			rc;
+#endif
+
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapurl", "ldap");
+#ifdef LDAP_API_FEATURE_X_OPENLDAP
+		rc = ldap_url_parse(val, &urldata);
+		if (rc != LDAP_SUCCESS)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("could not parse LDAP URL \"%s\": %s", val, ldap_err2string(rc))));
+			*err_msg = psprintf("could not parse LDAP URL \"%s\": %s",
+								val, ldap_err2string(rc));
+			return false;
+		}
+
+		if (strcmp(urldata->lud_scheme, "ldap") != 0 &&
+			strcmp(urldata->lud_scheme, "ldaps") != 0)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("unsupported LDAP URL scheme: %s", urldata->lud_scheme)));
+			*err_msg = psprintf("unsupported LDAP URL scheme: %s",
+								urldata->lud_scheme);
+			ldap_free_urldesc(urldata);
+			return false;
+		}
+
+		if (urldata->lud_scheme)
+			hbaline->ldapscheme = pstrdup(urldata->lud_scheme);
+		if (urldata->lud_host)
+			hbaline->ldapserver = pstrdup(urldata->lud_host);
+		hbaline->ldapport = urldata->lud_port;
+		if (urldata->lud_dn)
+			hbaline->ldapbasedn = pstrdup(urldata->lud_dn);
+
+		if (urldata->lud_attrs)
+			hbaline->ldapsearchattribute = pstrdup(urldata->lud_attrs[0]);	/* only use first one */
+		hbaline->ldapscope = urldata->lud_scope;
+		if (urldata->lud_filter)
+			hbaline->ldapsearchfilter = pstrdup(urldata->lud_filter);
+		ldap_free_urldesc(urldata);
+#else							/* not OpenLDAP */
 		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("end-of-line before database specification"),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = "end-of-line before database specification";
-		return NULL;
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("LDAP URLs not supported on this platform")));
+		*err_msg = "LDAP URLs not supported on this platform";
+#endif							/* not OpenLDAP */
 	}
-	parsedline->databases = NIL;
-	tokens = lfirst(field);
-	foreach(tokencell, tokens)
+	else if (strcmp(name, "ldaptls") == 0)
 	{
-		parsedline->databases = lappend(parsedline->databases,
-										copy_hba_token(lfirst(tokencell)));
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldaptls", "ldap");
+		if (strcmp(val, "1") == 0)
+			hbaline->ldaptls = true;
+		else
+			hbaline->ldaptls = false;
 	}
-
-	/* Get the roles. */
-	field = lnext(tok_line->fields, field);
-	if (!field)
+	else if (strcmp(name, "ldapscheme") == 0)
 	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("end-of-line before role specification"),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = "end-of-line before role specification";
-		return NULL;
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapscheme", "ldap");
+		if (strcmp(val, "ldap") != 0 && strcmp(val, "ldaps") != 0)
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("invalid ldapscheme value: \"%s\"", val),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+		hbaline->ldapscheme = pstrdup(val);
 	}
-	parsedline->roles = NIL;
-	tokens = lfirst(field);
-	foreach(tokencell, tokens)
+	else if (strcmp(name, "ldapserver") == 0)
 	{
-		parsedline->roles = lappend(parsedline->roles,
-									copy_hba_token(lfirst(tokencell)));
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap");
+		hbaline->ldapserver = pstrdup(val);
 	}
-
-	if (parsedline->conntype != ctLocal)
+	else if (strcmp(name, "ldapport") == 0)
 	{
-		/* Read the IP address field. (with or without CIDR netmask) */
-		field = lnext(tok_line->fields, field);
-		if (!field)
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapport", "ldap");
+		hbaline->ldapport = atoi(val);
+		if (hbaline->ldapport == 0)
 		{
 			ereport(elevel,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("end-of-line before IP address specification"),
+					 errmsg("invalid LDAP port number: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
 								line_num, HbaFileName)));
-			*err_msg = "end-of-line before IP address specification";
-			return NULL;
+			*err_msg = psprintf("invalid LDAP port number: \"%s\"", val);
+			return false;
 		}
-		tokens = lfirst(field);
-		if (tokens->length > 1)
+	}
+	else if (strcmp(name, "ldapbinddn") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbinddn", "ldap");
+		hbaline->ldapbinddn = pstrdup(val);
+	}
+	else if (strcmp(name, "ldapbindpasswd") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbindpasswd", "ldap");
+		hbaline->ldapbindpasswd = pstrdup(val);
+	}
+	else if (strcmp(name, "ldapsearchattribute") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
+		hbaline->ldapsearchattribute = pstrdup(val);
+	}
+	else if (strcmp(name, "ldapsearchfilter") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap");
+		hbaline->ldapsearchfilter = pstrdup(val);
+	}
+	else if (strcmp(name, "ldapbasedn") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
+		hbaline->ldapbasedn = pstrdup(val);
+	}
+	else if (strcmp(name, "ldapprefix") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapprefix", "ldap");
+		hbaline->ldapprefix = pstrdup(val);
+	}
+	else if (strcmp(name, "ldapsuffix") == 0)
+	{
+		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsuffix", "ldap");
+		hbaline->ldapsuffix = pstrdup(val);
+	}
+	else if (strcmp(name, "krb_realm") == 0)
+	{
+		if (hbaline->auth_method != uaGSS &&
+			hbaline->auth_method != uaSSPI)
+			INVALID_AUTH_OPTION("krb_realm", gettext_noop("gssapi and sspi"));
+		hbaline->krb_realm = pstrdup(val);
+	}
+	else if (strcmp(name, "include_realm") == 0)
+	{
+		if (hbaline->auth_method != uaGSS &&
+			hbaline->auth_method != uaSSPI)
+			INVALID_AUTH_OPTION("include_realm", gettext_noop("gssapi and sspi"));
+		if (strcmp(val, "1") == 0)
+			hbaline->include_realm = true;
+		else
+			hbaline->include_realm = false;
+	}
+	else if (strcmp(name, "compat_realm") == 0)
+	{
+		if (hbaline->auth_method != uaSSPI)
+			INVALID_AUTH_OPTION("compat_realm", gettext_noop("sspi"));
+		if (strcmp(val, "1") == 0)
+			hbaline->compat_realm = true;
+		else
+			hbaline->compat_realm = false;
+	}
+	else if (strcmp(name, "upn_username") == 0)
+	{
+		if (hbaline->auth_method != uaSSPI)
+			INVALID_AUTH_OPTION("upn_username", gettext_noop("sspi"));
+		if (strcmp(val, "1") == 0)
+			hbaline->upn_username = true;
+		else
+			hbaline->upn_username = false;
+	}
+	else if (strcmp(name, "radiusservers") == 0)
+	{
+		struct addrinfo *gai_result;
+		struct addrinfo hints;
+		int			ret;
+		List	   *parsed_servers;
+		ListCell   *l;
+		char	   *dupval = pstrdup(val);
+
+		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusservers", "radius");
+
+		if (!SplitGUCList(dupval, ',', &parsed_servers))
 		{
+			/* syntax error in list */
 			ereport(elevel,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("multiple values specified for host address"),
-					 errhint("Specify one address range per line."),
+					 errmsg("could not parse RADIUS server list \"%s\"",
+							val),
 					 errcontext("line %d of configuration file \"%s\"",
 								line_num, HbaFileName)));
-			*err_msg = "multiple values specified for host address";
-			return NULL;
+			return false;
 		}
-		token = linitial(tokens);
 
-		if (token_is_keyword(token, "all"))
-		{
-			parsedline->ip_cmp_method = ipCmpAll;
-		}
-		else if (token_is_keyword(token, "samehost"))
+		/* For each entry in the list, translate it */
+		foreach(l, parsed_servers)
 		{
-			/* Any IP on this host is allowed to connect */
-			parsedline->ip_cmp_method = ipCmpSameHost;
-		}
-		else if (token_is_keyword(token, "samenet"))
-		{
-			/* Any IP on the host's subnets is allowed to connect */
-			parsedline->ip_cmp_method = ipCmpSameNet;
-		}
-		else
-		{
-			/* IP and netmask are specified */
-			parsedline->ip_cmp_method = ipCmpMask;
-
-			/* need a modifiable copy of token */
-			str = pstrdup(token->string);
-
-			/* Check if it has a CIDR suffix and if so isolate it */
-			cidr_slash = strchr(str, '/');
-			if (cidr_slash)
-				*cidr_slash = '\0';
-
-			/* Get the IP address either way */
-			hints.ai_flags = AI_NUMERICHOST;
+			MemSet(&hints, 0, sizeof(hints));
+			hints.ai_socktype = SOCK_DGRAM;
 			hints.ai_family = AF_UNSPEC;
-			hints.ai_socktype = 0;
-			hints.ai_protocol = 0;
-			hints.ai_addrlen = 0;
-			hints.ai_canonname = NULL;
-			hints.ai_addr = NULL;
-			hints.ai_next = NULL;
 
-			ret = pg_getaddrinfo_all(str, NULL, &hints, &gai_result);
-			if (ret == 0 && gai_result)
-			{
-				memcpy(&parsedline->addr, gai_result->ai_addr,
-					   gai_result->ai_addrlen);
-				parsedline->addrlen = gai_result->ai_addrlen;
-			}
-			else if (ret == EAI_NONAME)
-				parsedline->hostname = str;
-			else
+			ret = pg_getaddrinfo_all((char *) lfirst(l), NULL, &hints, &gai_result);
+			if (ret || !gai_result)
 			{
 				ereport(elevel,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
-						 errmsg("invalid IP address \"%s\": %s",
-								str, gai_strerror(ret)),
+						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
+								(char *) lfirst(l), gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
 									line_num, HbaFileName)));
-				*err_msg = psprintf("invalid IP address \"%s\": %s",
-									str, gai_strerror(ret));
 				if (gai_result)
 					pg_freeaddrinfo_all(hints.ai_family, gai_result);
-				return NULL;
-			}
 
+				list_free(parsed_servers);
+				return false;
+			}
 			pg_freeaddrinfo_all(hints.ai_family, gai_result);
+		}
 
-			/* Get the netmask */
-			if (cidr_slash)
-			{
-				if (parsedline->hostname)
-				{
-					ereport(elevel,
-							(errcode(ERRCODE_CONFIG_FILE_ERROR),
-							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
-									token->string),
-							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
-					*err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
-										token->string);
-					return NULL;
-				}
+		/* All entries are OK, so store them */
+		hbaline->radiusservers = parsed_servers;
+		hbaline->radiusservers_s = pstrdup(val);
+	}
+	else if (strcmp(name, "radiusports") == 0)
+	{
+		List	   *parsed_ports;
+		ListCell   *l;
+		char	   *dupval = pstrdup(val);
 
-				if (pg_sockaddr_cidr_mask(&parsedline->mask, cidr_slash + 1,
-										  parsedline->addr.ss_family) < 0)
-				{
-					ereport(elevel,
-							(errcode(ERRCODE_CONFIG_FILE_ERROR),
-							 errmsg("invalid CIDR mask in address \"%s\"",
-									token->string),
-							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
-					*err_msg = psprintf("invalid CIDR mask in address \"%s\"",
-										token->string);
-					return NULL;
-				}
-				parsedline->masklen = parsedline->addrlen;
-				pfree(str);
-			}
-			else if (!parsedline->hostname)
-			{
-				/* Read the mask field. */
-				pfree(str);
-				field = lnext(tok_line->fields, field);
-				if (!field)
-				{
-					ereport(elevel,
-							(errcode(ERRCODE_CONFIG_FILE_ERROR),
-							 errmsg("end-of-line before netmask specification"),
-							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
-							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
-					*err_msg = "end-of-line before netmask specification";
-					return NULL;
-				}
-				tokens = lfirst(field);
-				if (tokens->length > 1)
-				{
-					ereport(elevel,
-							(errcode(ERRCODE_CONFIG_FILE_ERROR),
-							 errmsg("multiple values specified for netmask"),
-							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
-					*err_msg = "multiple values specified for netmask";
-					return NULL;
-				}
-				token = linitial(tokens);
+		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusports", "radius");
 
-				ret = pg_getaddrinfo_all(token->string, NULL,
-										 &hints, &gai_result);
-				if (ret || !gai_result)
-				{
-					ereport(elevel,
-							(errcode(ERRCODE_CONFIG_FILE_ERROR),
-							 errmsg("invalid IP mask \"%s\": %s",
-									token->string, gai_strerror(ret)),
-							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
-					*err_msg = psprintf("invalid IP mask \"%s\": %s",
-										token->string, gai_strerror(ret));
-					if (gai_result)
-						pg_freeaddrinfo_all(hints.ai_family, gai_result);
-					return NULL;
-				}
+		if (!SplitGUCList(dupval, ',', &parsed_ports))
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("could not parse RADIUS port list \"%s\"",
+							val),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
+			return false;
+		}
 
-				memcpy(&parsedline->mask, gai_result->ai_addr,
-					   gai_result->ai_addrlen);
-				parsedline->masklen = gai_result->ai_addrlen;
-				pg_freeaddrinfo_all(hints.ai_family, gai_result);
+		foreach(l, parsed_ports)
+		{
+			if (atoi(lfirst(l)) == 0)
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_CONFIG_FILE_ERROR),
+						 errmsg("invalid RADIUS port number: \"%s\"", val),
+						 errcontext("line %d of configuration file \"%s\"",
+									line_num, HbaFileName)));
 
-				if (parsedline->addr.ss_family != parsedline->mask.ss_family)
-				{
-					ereport(elevel,
-							(errcode(ERRCODE_CONFIG_FILE_ERROR),
-							 errmsg("IP address and mask do not match"),
-							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
-					*err_msg = "IP address and mask do not match";
-					return NULL;
-				}
+				return false;
 			}
 		}
-	}							/* != ctLocal */
-
-	/* Get the authentication method */
-	field = lnext(tok_line->fields, field);
-	if (!field)
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("end-of-line before authentication method"),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = "end-of-line before authentication method";
-		return NULL;
+		hbaline->radiusports = parsed_ports;
+		hbaline->radiusports_s = pstrdup(val);
 	}
-	tokens = lfirst(field);
-	if (tokens->length > 1)
+	else if (strcmp(name, "radiussecrets") == 0)
 	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("multiple values specified for authentication type"),
-				 errhint("Specify exactly one authentication type per line."),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = "multiple values specified for authentication type";
-		return NULL;
-	}
-	token = linitial(tokens);
+		List	   *parsed_secrets;
+		char	   *dupval = pstrdup(val);
 
-	unsupauth = NULL;
-	if (strcmp(token->string, "trust") == 0)
-		parsedline->auth_method = uaTrust;
-	else if (strcmp(token->string, "ident") == 0)
-		parsedline->auth_method = uaIdent;
-	else if (strcmp(token->string, "peer") == 0)
-		parsedline->auth_method = uaPeer;
-	else if (strcmp(token->string, "password") == 0)
-		parsedline->auth_method = uaPassword;
-	else if (strcmp(token->string, "gss") == 0)
-#ifdef ENABLE_GSS
-		parsedline->auth_method = uaGSS;
-#else
-		unsupauth = "gss";
-#endif
-	else if (strcmp(token->string, "sspi") == 0)
-#ifdef ENABLE_SSPI
-		parsedline->auth_method = uaSSPI;
-#else
-		unsupauth = "sspi";
-#endif
-	else if (strcmp(token->string, "reject") == 0)
-		parsedline->auth_method = uaReject;
-	else if (strcmp(token->string, "md5") == 0)
-	{
-		if (Db_user_namespace)
+		REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecrets", "radius");
+
+		if (!SplitGUCList(dupval, ',', &parsed_secrets))
 		{
+			/* syntax error in list */
 			ereport(elevel,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
+					 errmsg("could not parse RADIUS secret list \"%s\"",
+							val),
 					 errcontext("line %d of configuration file \"%s\"",
 								line_num, HbaFileName)));
-			*err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
-			return NULL;
+			return false;
 		}
-		parsedline->auth_method = uaMD5;
+
+		hbaline->radiussecrets = parsed_secrets;
+		hbaline->radiussecrets_s = pstrdup(val);
 	}
-	else if (strcmp(token->string, "scram-sha-256") == 0)
-		parsedline->auth_method = uaSCRAM;
-	else if (strcmp(token->string, "pam") == 0)
-#ifdef USE_PAM
-		parsedline->auth_method = uaPAM;
-#else
-		unsupauth = "pam";
-#endif
-	else if (strcmp(token->string, "bsd") == 0)
-#ifdef USE_BSD_AUTH
-		parsedline->auth_method = uaBSD;
-#else
-		unsupauth = "bsd";
-#endif
-	else if (strcmp(token->string, "ldap") == 0)
-#ifdef USE_LDAP
-		parsedline->auth_method = uaLDAP;
-#else
-		unsupauth = "ldap";
-#endif
-	else if (strcmp(token->string, "cert") == 0)
-#ifdef USE_SSL
-		parsedline->auth_method = uaCert;
-#else
-		unsupauth = "cert";
-#endif
-	else if (strcmp(token->string, "radius") == 0)
-		parsedline->auth_method = uaRADIUS;
-	else
+	else if (strcmp(name, "radiusidentifiers") == 0)
 	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("invalid authentication method \"%s\"",
-						token->string),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = psprintf("invalid authentication method \"%s\"",
-							token->string);
-		return NULL;
-	}
+		List	   *parsed_identifiers;
+		char	   *dupval = pstrdup(val);
 
-	if (unsupauth)
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("invalid authentication method \"%s\": not supported by this build",
-						token->string),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
-							token->string);
-		return NULL;
-	}
+		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifiers", "radius");
 
-	/*
-	 * XXX: When using ident on local connections, change it to peer, for
-	 * backwards compatibility.
-	 */
-	if (parsedline->conntype == ctLocal &&
-		parsedline->auth_method == uaIdent)
-		parsedline->auth_method = uaPeer;
+		if (!SplitGUCList(dupval, ',', &parsed_identifiers))
+		{
+			/* syntax error in list */
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("could not parse RADIUS identifiers list \"%s\"",
+							val),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			return false;
+		}
 
-	/* Invalid authentication combinations */
-	if (parsedline->conntype == ctLocal &&
-		parsedline->auth_method == uaGSS)
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("gssapi authentication is not supported on local sockets"),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = "gssapi authentication is not supported on local sockets";
-		return NULL;
+		hbaline->radiusidentifiers = parsed_identifiers;
+		hbaline->radiusidentifiers_s = pstrdup(val);
 	}
-
-	if (parsedline->conntype != ctLocal &&
-		parsedline->auth_method == uaPeer)
+	else
 	{
 		ereport(elevel,
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("peer authentication is only supported on local sockets"),
+				 errmsg("unrecognized authentication option name: \"%s\"",
+						name),
 				 errcontext("line %d of configuration file \"%s\"",
 							line_num, HbaFileName)));
-		*err_msg = "peer authentication is only supported on local sockets";
-		return NULL;
+		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
+							name);
+		return false;
 	}
+	return true;
+}
 
-	/*
-	 * SSPI authentication can never be enabled on ctLocal connections,
-	 * because it's only supported on Windows, where ctLocal isn't supported.
-	 */
-
-
-	if (parsedline->conntype != ctHostSSL &&
-		parsedline->auth_method == uaCert)
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("cert authentication is only supported on hostssl connections"),
-				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
-		*err_msg = "cert authentication is only supported on hostssl connections";
-		return NULL;
-	}
+/*
+ *	Scan the pre-parsed hba file, looking for a match to the port's connection
+ *	request.
+ */
+static void
+check_hba(hbaPort *port)
+{
+	Oid			roleid;
+	ListCell   *line;
+	HbaLine    *hba;
 
-	/*
-	 * For GSS and SSPI, set the default value of include_realm to true.
-	 * Having include_realm set to false is dangerous in multi-realm
-	 * situations and is generally considered bad practice.  We keep the
-	 * capability around for backwards compatibility, but we might want to
-	 * remove it at some point in the future.  Users who still need to strip
-	 * the realm off would be better served by using an appropriate regex in a
-	 * pg_ident.conf mapping.
-	 */
-	if (parsedline->auth_method == uaGSS ||
-		parsedline->auth_method == uaSSPI)
-		parsedline->include_realm = true;
+	/* Get the target role's OID.  Note we do not error out for bad role. */
+	roleid = get_role_oid(port->user_name, true);
 
-	/*
-	 * For SSPI, include_realm defaults to the SAM-compatible domain (aka
-	 * NetBIOS name) and user names instead of the Kerberos principal name for
-	 * compatibility.
-	 */
-	if (parsedline->auth_method == uaSSPI)
+	foreach(line, parsed_hba_lines)
 	{
-		parsedline->compat_realm = true;
-		parsedline->upn_username = false;
-	}
+		hba = (HbaLine *) lfirst(line);
 
-	/* Parse remaining arguments */
-	while ((field = lnext(tok_line->fields, field)) != NULL)
-	{
-		tokens = lfirst(field);
-		foreach(tokencell, tokens)
+		/* Check connection type */
+		if (hba->conntype == ctLocal)
 		{
-			char	   *val;
-
-			token = lfirst(tokencell);
+			if (port->raddr.addr.ss_family != AF_UNIX)
+				continue;
+		}
+		else
+		{
+			if (port->raddr.addr.ss_family == AF_UNIX)
+				continue;
 
-			str = pstrdup(token->string);
-			val = strchr(str, '=');
-			if (val == NULL)
+			/* Check SSL state */
+			if (port->ssl_in_use)
 			{
-				/*
-				 * Got something that's not a name=value pair.
-				 */
-				ereport(elevel,
-						(errcode(ERRCODE_CONFIG_FILE_ERROR),
-						 errmsg("authentication option not in name=value format: %s", token->string),
-						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
-				*err_msg = psprintf("authentication option not in name=value format: %s",
-									token->string);
-				return NULL;
+				/* Connection is SSL, match both "host" and "hostssl" */
+				if (hba->conntype == ctHostNoSSL)
+					continue;
+			}
+			else
+			{
+				/* Connection is not SSL, match both "host" and "hostnossl" */
+				if (hba->conntype == ctHostSSL)
+					continue;
 			}
 
-			*val++ = '\0';		/* str now holds "name", val holds "value" */
-			if (!parse_hba_auth_opt(str, val, parsedline, elevel, err_msg))
-				/* parse_hba_auth_opt already logged the error message */
-				return NULL;
-			pfree(str);
-		}
-	}
-
-	/*
-	 * Check if the selected authentication method has any mandatory arguments
-	 * that are not set.
-	 */
-	if (parsedline->auth_method == uaLDAP)
-	{
-#ifndef HAVE_LDAP_INITIALIZE
-		/* Not mandatory for OpenLDAP, because it can use DNS SRV records */
-		MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap");
+			/* Check GSSAPI state */
+#ifdef ENABLE_GSS
+			if (port->gss && port->gss->enc &&
+				hba->conntype == ctHostNoGSS)
+				continue;
+			else if (!(port->gss && port->gss->enc) &&
+					 hba->conntype == ctHostGSS)
+				continue;
+#else
+			if (hba->conntype == ctHostGSS)
+				continue;
 #endif
 
-		/*
-		 * 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 one of
-		 * ldapsearchattribute or ldapsearchfilter.  Disallow mixing these
-		 * parameters.
-		 */
-		if (parsedline->ldapprefix || parsedline->ldapsuffix)
-		{
-			if (parsedline->ldapbasedn ||
-				parsedline->ldapbinddn ||
-				parsedline->ldapbindpasswd ||
-				parsedline->ldapsearchattribute ||
-				parsedline->ldapsearchfilter)
+			/* Check IP address */
+			switch (hba->ip_cmp_method)
 			{
-				ereport(elevel,
-						(errcode(ERRCODE_CONFIG_FILE_ERROR),
-						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
-						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
-				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
-				return NULL;
+				case ipCmpMask:
+					if (hba->hostname)
+					{
+						if (!check_hostname(port,
+											hba->hostname))
+							continue;
+					}
+					else
+					{
+						if (!check_ip(&port->raddr,
+									  (struct sockaddr *) &hba->addr,
+									  (struct sockaddr *) &hba->mask))
+							continue;
+					}
+					break;
+				case ipCmpAll:
+					break;
+				case ipCmpSameHost:
+				case ipCmpSameNet:
+					if (!check_same_host_or_net(&port->raddr,
+												hba->ip_cmp_method))
+						continue;
+					break;
+				default:
+					/* shouldn't get here, but deem it no-match if so */
+					continue;
 			}
-		}
-		else if (!parsedline->ldapbasedn)
-		{
-			ereport(elevel,
-					(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)));
-			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
-			return NULL;
-		}
+		}						/* != ctLocal */
 
-		/*
-		 * When using search+bind, you can either use a simple attribute
-		 * (defaulting to "uid") or a fully custom search filter.  You can't
-		 * do both.
-		 */
-		if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
-			return NULL;
-		}
+		/* Check database and role */
+		if (!check_db(port->database_name, port->user_name, roleid,
+					  hba->databases))
+			continue;
+
+		if (!check_role(port->user_name, roleid, hba->roles))
+			continue;
+
+		/* Found a record that matched! */
+		port->hba = hba;
+		return;
 	}
 
-	if (parsedline->auth_method == uaRADIUS)
+	/* If no matching entry was found, then implicitly reject. */
+	hba = palloc0(sizeof(HbaLine));
+	hba->auth_method = uaImplicitReject;
+	port->hba = hba;
+}
+
+/*
+ * Read the config file and create a List of HbaLine records for the contents.
+ *
+ * The configuration is read into a temporary list, and if any parse error
+ * occurs the old list is kept in place and false is returned.  Only if the
+ * whole file parses OK is the list replaced, and the function returns true.
+ *
+ * On a false result, caller will take care of reporting a FATAL error in case
+ * this is the initial startup.  If it happens on reload, we just keep running
+ * with the old data.
+ */
+bool
+load_hba(void)
+{
+	FILE	   *file;
+	List	   *hba_lines = NIL;
+	ListCell   *line;
+	List	   *new_parsed_lines = NIL;
+	bool		ok = true;
+	MemoryContext linecxt;
+	MemoryContext oldcxt;
+	MemoryContext hbacxt;
+
+	file = AllocateFile(HbaFileName, "r");
+	if (file == NULL)
 	{
-		MANDATORY_AUTH_ARG(parsedline->radiusservers, "radiusservers", "radius");
-		MANDATORY_AUTH_ARG(parsedline->radiussecrets, "radiussecrets", "radius");
+		ereport(LOG,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration file \"%s\": %m",
+						HbaFileName)));
+		return false;
+	}
 
-		if (list_length(parsedline->radiusservers) < 1)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("list of RADIUS servers cannot be empty"),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = "list of RADIUS servers cannot be empty";
-			return NULL;
-		}
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	FreeFile(file);
 
-		if (list_length(parsedline->radiussecrets) < 1)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("list of RADIUS secrets cannot be empty"),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = "list of RADIUS secrets cannot be empty";
-			return NULL;
-		}
+	/* Now parse all the lines */
+	Assert(PostmasterContext);
+	hbacxt = AllocSetContextCreate(PostmasterContext,
+								   "hba parser context",
+								   ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hbacxt);
+	foreach(line, hba_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		HbaLine    *newline;
 
-		/*
-		 * Verify length of option lists - each can be 0 (except for secrets,
-		 * but that's already checked above), 1 (use the same value
-		 * everywhere) or the same as the number of servers.
-		 */
-		if (!(list_length(parsedline->radiussecrets) == 1 ||
-			  list_length(parsedline->radiussecrets) == list_length(parsedline->radiusservers)))
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
-							list_length(parsedline->radiussecrets),
-							list_length(parsedline->radiusservers)),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
-								list_length(parsedline->radiussecrets),
-								list_length(parsedline->radiusservers));
-			return NULL;
-		}
-		if (!(list_length(parsedline->radiusports) == 0 ||
-			  list_length(parsedline->radiusports) == 1 ||
-			  list_length(parsedline->radiusports) == list_length(parsedline->radiusservers)))
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg != NULL)
 		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
-							list_length(parsedline->radiusports),
-							list_length(parsedline->radiusservers)),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
-								list_length(parsedline->radiusports),
-								list_length(parsedline->radiusservers));
-			return NULL;
+			ok = false;
+			continue;
 		}
-		if (!(list_length(parsedline->radiusidentifiers) == 0 ||
-			  list_length(parsedline->radiusidentifiers) == 1 ||
-			  list_length(parsedline->radiusidentifiers) == list_length(parsedline->radiusservers)))
+
+		if ((newline = parse_hba_line(tok_line, LOG)) == NULL)
 		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
-							list_length(parsedline->radiusidentifiers),
-							list_length(parsedline->radiusservers)),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
-								list_length(parsedline->radiusidentifiers),
-								list_length(parsedline->radiusservers));
-			return NULL;
+			/* Parse error; remember there's trouble */
+			ok = false;
+
+			/*
+			 * Keep parsing the rest of the file so we can report errors on
+			 * more than the first line.  Error has already been logged, no
+			 * need for more chatter here.
+			 */
+			continue;
 		}
+
+		new_parsed_lines = lappend(new_parsed_lines, newline);
 	}
 
 	/*
-	 * Enforce any parameters implied by other settings.
+	 * A valid HBA file must have at least one entry; else there's no way to
+	 * connect to the postmaster.  But only complain about this if we didn't
+	 * already have parsing errors.
 	 */
-	if (parsedline->auth_method == uaCert)
+	if (ok && new_parsed_lines == NIL)
 	{
-		/*
-		 * For auth method cert, client certificate validation is mandatory, and it implies
-		 * the level of verify-full.
-		 */
-		parsedline->clientcert = clientCertFull;
+		ereport(LOG,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("configuration file \"%s\" contains no entries",
+						HbaFileName)));
+		ok = false;
 	}
 
-	return parsedline;
-}
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	MemoryContextSwitchTo(oldcxt);
+
+	if (!ok)
+	{
+		/* File contained one or more errors, so bail out */
+		MemoryContextDelete(hbacxt);
+		return false;
+	}
+
+	/* Loaded new file successfully, replace the one we use */
+	if (parsed_hba_context != NULL)
+		MemoryContextDelete(parsed_hba_context);
+	parsed_hba_context = hbacxt;
+	parsed_hba_lines = new_parsed_lines;
 
+	return true;
+}
 
 /*
- * Parse one name-value pair as an authentication option into the given
- * HbaLine.  Return true if we successfully parse the option, false if we
- * encounter an error.  In the event of an error, also log a message at
- * ereport level elevel, and store a message string into *err_msg.
+ * Parse one tokenised line from the ident config file and store the result in
+ * an IdentLine structure.
+ *
+ * If parsing fails, log a message and return NULL.
+ *
+ * If ident_user is a regular expression (ie. begins with a slash), it is
+ * compiled and stored in IdentLine structure.
+ *
+ * Note: this function leaks memory when an error occurs.  Caller is expected
+ * to have set a memory context that will be reset if this function returns
+ * NULL.
  */
-static bool
-parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
-				   int elevel, char **err_msg)
+static IdentLine *
+parse_ident_line(TokenizedAuthLine *tok_line)
 {
-	int			line_num = hbaline->linenumber;
+	int			line_num = tok_line->line_num;
+	ListCell   *field;
+	List	   *tokens;
+	HbaToken   *token;
+	IdentLine  *parsedline;
 
-#ifdef USE_LDAP
-	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
-#endif
+	Assert(tok_line->fields != NIL);
+	field = list_head(tok_line->fields);
 
-	if (strcmp(name, "map") == 0)
-	{
-		if (hbaline->auth_method != uaIdent &&
-			hbaline->auth_method != uaPeer &&
-			hbaline->auth_method != uaGSS &&
-			hbaline->auth_method != uaSSPI &&
-			hbaline->auth_method != uaCert)
-			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, and cert"));
-		hbaline->usermap = pstrdup(val);
-	}
-	else if (strcmp(name, "clientcert") == 0)
-	{
-		if (hbaline->conntype != ctHostSSL)
-		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = "clientcert can only be configured for \"hostssl\" rows";
-			return false;
-		}
+	parsedline = palloc0(sizeof(IdentLine));
+	parsedline->linenumber = line_num;
 
-		if (strcmp(val, "verify-full") == 0)
+	/* Get the map token (must exist) */
+	tokens = lfirst(field);
+	IDENT_MULTI_VALUE(tokens);
+	token = linitial(tokens);
+	parsedline->usermap = pstrdup(token->string);
+
+	/* Get the ident user token */
+	field = lnext(tok_line->fields, field);
+	IDENT_FIELD_ABSENT(field);
+	tokens = lfirst(field);
+	IDENT_MULTI_VALUE(tokens);
+	token = linitial(tokens);
+	parsedline->ident_user = pstrdup(token->string);
+
+	/* Get the PG rolename token */
+	field = lnext(tok_line->fields, field);
+	IDENT_FIELD_ABSENT(field);
+	tokens = lfirst(field);
+	IDENT_MULTI_VALUE(tokens);
+	token = linitial(tokens);
+	parsedline->pg_role = pstrdup(token->string);
+
+	if (parsedline->ident_user[0] == '/')
+	{
+		/*
+		 * When system username starts with a slash, treat it as a regular
+		 * expression. Pre-compile it.
+		 */
+		int			r;
+		pg_wchar   *wstr;
+		int			wlen;
+
+		wstr = palloc((strlen(parsedline->ident_user + 1) + 1) * sizeof(pg_wchar));
+		wlen = pg_mb2wchar_with_len(parsedline->ident_user + 1,
+									wstr, strlen(parsedline->ident_user + 1));
+
+		r = pg_regcomp(&parsedline->re, wstr, wlen, REG_ADVANCED, C_COLLATION_OID);
+		if (r)
 		{
-			hbaline->clientcert = clientCertFull;
+			char		errstr[100];
+
+			pg_regerror(r, &parsedline->re, errstr, sizeof(errstr));
+			ereport(LOG,
+					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+					 errmsg("invalid regular expression \"%s\": %s",
+							parsedline->ident_user + 1, errstr)));
+
+			pfree(wstr);
+			return NULL;
 		}
-		else if (strcmp(val, "verify-ca") == 0)
+		pfree(wstr);
+	}
+
+	return parsedline;
+}
+
+/*
+ *	Process one line from the parsed ident config lines.
+ *
+ *	Compare input parsed ident line to the needed map, pg_role and ident_user.
+ *	*found_p and *error_p are set according to our results.
+ */
+static void
+check_ident_usermap(IdentLine *identLine, const char *usermap_name,
+					const char *pg_role, const char *ident_user,
+					bool case_insensitive, bool *found_p, bool *error_p)
+{
+	*found_p = false;
+	*error_p = false;
+
+	if (strcmp(identLine->usermap, usermap_name) != 0)
+		/* Line does not match the map name we're looking for, so just abort */
+		return;
+
+	/* Match? */
+	if (identLine->ident_user[0] == '/')
+	{
+		/*
+		 * When system username starts with a slash, treat it as a regular
+		 * expression. In this case, we process the system username as a
+		 * regular expression that returns exactly one match. This is replaced
+		 * for \1 in the database username string, if present.
+		 */
+		int			r;
+		regmatch_t	matches[2];
+		pg_wchar   *wstr;
+		int			wlen;
+		char	   *ofs;
+		char	   *regexp_pgrole;
+
+		wstr = palloc((strlen(ident_user) + 1) * sizeof(pg_wchar));
+		wlen = pg_mb2wchar_with_len(ident_user, wstr, strlen(ident_user));
+
+		r = pg_regexec(&identLine->re, wstr, wlen, 0, NULL, 2, matches, 0);
+		if (r)
 		{
-			if (hbaline->auth_method == uaCert)
+			char		errstr[100];
+
+			if (r != REG_NOMATCH)
 			{
-				ereport(elevel,
-						(errcode(ERRCODE_CONFIG_FILE_ERROR),
-						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
-						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
-				*err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
-				return false;
+				/* REG_NOMATCH is not an error, everything else is */
+				pg_regerror(r, &identLine->re, errstr, sizeof(errstr));
+				ereport(LOG,
+						(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+						 errmsg("regular expression match for \"%s\" failed: %s",
+								identLine->ident_user + 1, errstr)));
+				*error_p = true;
 			}
 
-			hbaline->clientcert = clientCertCA;
+			pfree(wstr);
+			return;
 		}
-		else
+		pfree(wstr);
+
+		if ((ofs = strstr(identLine->pg_role, "\\1")) != NULL)
 		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("invalid value for clientcert: \"%s\"", val),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			return false;
+			int			offset;
+
+			/* substitution of the first argument requested */
+			if (matches[1].rm_so < 0)
+			{
+				ereport(LOG,
+						(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+						 errmsg("regular expression \"%s\" has no subexpressions as requested by backreference in \"%s\"",
+								identLine->ident_user + 1, identLine->pg_role)));
+				*error_p = true;
+				return;
+			}
+
+			/*
+			 * 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);
+			memcpy(regexp_pgrole + offset,
+				   ident_user + matches[1].rm_so,
+				   matches[1].rm_eo - matches[1].rm_so);
+			strcat(regexp_pgrole, ofs + 2);
 		}
-	}
-	else if (strcmp(name, "clientname") == 0)
-	{
-		if (hbaline->conntype != ctHostSSL)
+		else
 		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("clientname can only be configured for \"hostssl\" rows"),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = "clientname can only be configured for \"hostssl\" rows";
-			return false;
+			/* no substitution, so copy the match */
+			regexp_pgrole = pstrdup(identLine->pg_role);
 		}
 
-		if (strcmp(val, "CN") == 0)
-		{
-			hbaline->clientcertname = clientCertCN;
-		}
-		else if (strcmp(val, "DN") == 0)
+		/*
+		 * now check if the username actually matched what the user is trying
+		 * to connect as
+		 */
+		if (case_insensitive)
 		{
-			hbaline->clientcertname = clientCertDN;
+			if (pg_strcasecmp(regexp_pgrole, pg_role) == 0)
+				*found_p = true;
 		}
 		else
 		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("invalid value for clientname: \"%s\"", val),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			return false;
+			if (strcmp(regexp_pgrole, pg_role) == 0)
+				*found_p = true;
 		}
-	}
-	else if (strcmp(name, "pamservice") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaPAM, "pamservice", "pam");
-		hbaline->pamservice = pstrdup(val);
-	}
-	else if (strcmp(name, "pam_use_hostname") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaPAM, "pam_use_hostname", "pam");
-		if (strcmp(val, "1") == 0)
-			hbaline->pam_use_hostname = true;
-		else
-			hbaline->pam_use_hostname = false;
+		pfree(regexp_pgrole);
 
+		return;
 	}
-	else if (strcmp(name, "ldapurl") == 0)
+	else
 	{
-#ifdef LDAP_API_FEATURE_X_OPENLDAP
-		LDAPURLDesc *urldata;
-		int			rc;
-#endif
-
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapurl", "ldap");
-#ifdef LDAP_API_FEATURE_X_OPENLDAP
-		rc = ldap_url_parse(val, &urldata);
-		if (rc != LDAP_SUCCESS)
+		/* Not regular expression, so make complete match */
+		if (case_insensitive)
 		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("could not parse LDAP URL \"%s\": %s", val, ldap_err2string(rc))));
-			*err_msg = psprintf("could not parse LDAP URL \"%s\": %s",
-								val, ldap_err2string(rc));
-			return false;
+			if (pg_strcasecmp(identLine->pg_role, pg_role) == 0 &&
+				pg_strcasecmp(identLine->ident_user, ident_user) == 0)
+				*found_p = true;
 		}
-
-		if (strcmp(urldata->lud_scheme, "ldap") != 0 &&
-			strcmp(urldata->lud_scheme, "ldaps") != 0)
+		else
 		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("unsupported LDAP URL scheme: %s", urldata->lud_scheme)));
-			*err_msg = psprintf("unsupported LDAP URL scheme: %s",
-								urldata->lud_scheme);
-			ldap_free_urldesc(urldata);
-			return false;
+			if (strcmp(identLine->pg_role, pg_role) == 0 &&
+				strcmp(identLine->ident_user, ident_user) == 0)
+				*found_p = true;
 		}
+	}
+}
 
-		if (urldata->lud_scheme)
-			hbaline->ldapscheme = pstrdup(urldata->lud_scheme);
-		if (urldata->lud_host)
-			hbaline->ldapserver = pstrdup(urldata->lud_host);
-		hbaline->ldapport = urldata->lud_port;
-		if (urldata->lud_dn)
-			hbaline->ldapbasedn = pstrdup(urldata->lud_dn);
 
-		if (urldata->lud_attrs)
-			hbaline->ldapsearchattribute = pstrdup(urldata->lud_attrs[0]);	/* only use first one */
-		hbaline->ldapscope = urldata->lud_scope;
-		if (urldata->lud_filter)
-			hbaline->ldapsearchfilter = pstrdup(urldata->lud_filter);
-		ldap_free_urldesc(urldata);
-#else							/* not OpenLDAP */
-		ereport(elevel,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("LDAP URLs not supported on this platform")));
-		*err_msg = "LDAP URLs not supported on this platform";
-#endif							/* not OpenLDAP */
-	}
-	else if (strcmp(name, "ldaptls") == 0)
+/*
+ *	Scan the (pre-parsed) ident usermap file line by line, looking for a match
+ *
+ *	See if the user with ident username "auth_user" is allowed to act
+ *	as Postgres user "pg_role" according to usermap "usermap_name".
+ *
+ *	Special case: Usermap NULL, equivalent to what was previously called
+ *	"sameuser" or "samerole", means don't look in the usermap file.
+ *	That's an implied map wherein "pg_role" must be identical to
+ *	"auth_user" in order to be authorized.
+ *
+ *	Iff authorized, return STATUS_OK, otherwise return STATUS_ERROR.
+ */
+int
+check_usermap(const char *usermap_name,
+			  const char *pg_role,
+			  const char *auth_user,
+			  bool case_insensitive)
+{
+	bool		found_entry = false,
+				error = false;
+
+	if (usermap_name == NULL || usermap_name[0] == '\0')
 	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldaptls", "ldap");
-		if (strcmp(val, "1") == 0)
-			hbaline->ldaptls = true;
+		if (case_insensitive)
+		{
+			if (pg_strcasecmp(pg_role, auth_user) == 0)
+				return STATUS_OK;
+		}
 		else
-			hbaline->ldaptls = false;
-	}
-	else if (strcmp(name, "ldapscheme") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapscheme", "ldap");
-		if (strcmp(val, "ldap") != 0 && strcmp(val, "ldaps") != 0)
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("invalid ldapscheme value: \"%s\"", val),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-		hbaline->ldapscheme = pstrdup(val);
-	}
-	else if (strcmp(name, "ldapserver") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap");
-		hbaline->ldapserver = pstrdup(val);
-	}
-	else if (strcmp(name, "ldapport") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapport", "ldap");
-		hbaline->ldapport = atoi(val);
-		if (hbaline->ldapport == 0)
 		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("invalid LDAP port number: \"%s\"", val),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = psprintf("invalid LDAP port number: \"%s\"", val);
-			return false;
+			if (strcmp(pg_role, auth_user) == 0)
+				return STATUS_OK;
 		}
+		ereport(LOG,
+				(errmsg("provided user name (%s) and authenticated user name (%s) do not match",
+						pg_role, auth_user)));
+		return STATUS_ERROR;
 	}
-	else if (strcmp(name, "ldapbinddn") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbinddn", "ldap");
-		hbaline->ldapbinddn = pstrdup(val);
-	}
-	else if (strcmp(name, "ldapbindpasswd") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbindpasswd", "ldap");
-		hbaline->ldapbindpasswd = pstrdup(val);
-	}
-	else if (strcmp(name, "ldapsearchattribute") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
-		hbaline->ldapsearchattribute = pstrdup(val);
-	}
-	else if (strcmp(name, "ldapsearchfilter") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap");
-		hbaline->ldapsearchfilter = pstrdup(val);
-	}
-	else if (strcmp(name, "ldapbasedn") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
-		hbaline->ldapbasedn = pstrdup(val);
-	}
-	else if (strcmp(name, "ldapprefix") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapprefix", "ldap");
-		hbaline->ldapprefix = pstrdup(val);
-	}
-	else if (strcmp(name, "ldapsuffix") == 0)
-	{
-		REQUIRE_AUTH_OPTION(uaLDAP, "ldapsuffix", "ldap");
-		hbaline->ldapsuffix = pstrdup(val);
-	}
-	else if (strcmp(name, "krb_realm") == 0)
-	{
-		if (hbaline->auth_method != uaGSS &&
-			hbaline->auth_method != uaSSPI)
-			INVALID_AUTH_OPTION("krb_realm", gettext_noop("gssapi and sspi"));
-		hbaline->krb_realm = pstrdup(val);
-	}
-	else if (strcmp(name, "include_realm") == 0)
+	else
 	{
-		if (hbaline->auth_method != uaGSS &&
-			hbaline->auth_method != uaSSPI)
-			INVALID_AUTH_OPTION("include_realm", gettext_noop("gssapi and sspi"));
-		if (strcmp(val, "1") == 0)
-			hbaline->include_realm = true;
-		else
-			hbaline->include_realm = false;
+		ListCell   *line_cell;
+
+		foreach(line_cell, parsed_ident_lines)
+		{
+			check_ident_usermap(lfirst(line_cell), usermap_name,
+								pg_role, auth_user, case_insensitive,
+								&found_entry, &error);
+			if (found_entry || error)
+				break;
+		}
 	}
-	else if (strcmp(name, "compat_realm") == 0)
+	if (!found_entry && !error)
 	{
-		if (hbaline->auth_method != uaSSPI)
-			INVALID_AUTH_OPTION("compat_realm", gettext_noop("sspi"));
-		if (strcmp(val, "1") == 0)
-			hbaline->compat_realm = true;
-		else
-			hbaline->compat_realm = false;
+		ereport(LOG,
+				(errmsg("no match in usermap \"%s\" for user \"%s\" authenticated as \"%s\"",
+						usermap_name, pg_role, auth_user)));
 	}
-	else if (strcmp(name, "upn_username") == 0)
+	return found_entry ? STATUS_OK : STATUS_ERROR;
+}
+
+
+/*
+ * Read the ident config file and create a List of IdentLine records for
+ * the contents.
+ *
+ * This works the same as load_hba(), but for the user config file.
+ */
+bool
+load_ident(void)
+{
+	FILE	   *file;
+	List	   *ident_lines = NIL;
+	ListCell   *line_cell,
+			   *parsed_line_cell;
+	List	   *new_parsed_lines = NIL;
+	bool		ok = true;
+	MemoryContext linecxt;
+	MemoryContext oldcxt;
+	MemoryContext ident_context;
+	IdentLine  *newline;
+
+	file = AllocateFile(IdentFileName, "r");
+	if (file == NULL)
 	{
-		if (hbaline->auth_method != uaSSPI)
-			INVALID_AUTH_OPTION("upn_username", gettext_noop("sspi"));
-		if (strcmp(val, "1") == 0)
-			hbaline->upn_username = true;
-		else
-			hbaline->upn_username = false;
+		/* not fatal ... we just won't do any special ident maps */
+		ereport(LOG,
+				(errcode_for_file_access(),
+				 errmsg("could not open usermap file \"%s\": %m",
+						IdentFileName)));
+		return false;
 	}
-	else if (strcmp(name, "radiusservers") == 0)
-	{
-		struct addrinfo *gai_result;
-		struct addrinfo hints;
-		int			ret;
-		List	   *parsed_servers;
-		ListCell   *l;
-		char	   *dupval = pstrdup(val);
 
-		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusservers", "radius");
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	FreeFile(file);
 
-		if (!SplitGUCList(dupval, ',', &parsed_servers))
+	/* Now parse all the lines */
+	Assert(PostmasterContext);
+	ident_context = AllocSetContextCreate(PostmasterContext,
+										  "ident parser context",
+										  ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(ident_context);
+	foreach(line_cell, ident_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line_cell);
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg != NULL)
 		{
-			/* syntax error in list */
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("could not parse RADIUS server list \"%s\"",
-							val),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			return false;
+			ok = false;
+			continue;
 		}
 
-		/* For each entry in the list, translate it */
-		foreach(l, parsed_servers)
+		if ((newline = parse_ident_line(tok_line)) == NULL)
 		{
-			MemSet(&hints, 0, sizeof(hints));
-			hints.ai_socktype = SOCK_DGRAM;
-			hints.ai_family = AF_UNSPEC;
-
-			ret = pg_getaddrinfo_all((char *) lfirst(l), NULL, &hints, &gai_result);
-			if (ret || !gai_result)
-			{
-				ereport(elevel,
-						(errcode(ERRCODE_CONFIG_FILE_ERROR),
-						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
-								(char *) lfirst(l), gai_strerror(ret)),
-						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
-				if (gai_result)
-					pg_freeaddrinfo_all(hints.ai_family, gai_result);
+			/* Parse error; remember there's trouble */
+			ok = false;
 
-				list_free(parsed_servers);
-				return false;
-			}
-			pg_freeaddrinfo_all(hints.ai_family, gai_result);
+			/*
+			 * Keep parsing the rest of the file so we can report errors on
+			 * more than the first line.  Error has already been logged, no
+			 * need for more chatter here.
+			 */
+			continue;
 		}
 
-		/* All entries are OK, so store them */
-		hbaline->radiusservers = parsed_servers;
-		hbaline->radiusservers_s = pstrdup(val);
+		new_parsed_lines = lappend(new_parsed_lines, newline);
 	}
-	else if (strcmp(name, "radiusports") == 0)
-	{
-		List	   *parsed_ports;
-		ListCell   *l;
-		char	   *dupval = pstrdup(val);
 
-		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusports", "radius");
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	MemoryContextSwitchTo(oldcxt);
 
-		if (!SplitGUCList(dupval, ',', &parsed_ports))
+	if (!ok)
+	{
+		/*
+		 * File contained one or more errors, so bail out, first being careful
+		 * to clean up whatever we allocated.  Most stuff will go away via
+		 * MemoryContextDelete, but we have to clean up regexes explicitly.
+		 */
+		foreach(parsed_line_cell, new_parsed_lines)
 		{
-			ereport(elevel,
-					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("could not parse RADIUS port list \"%s\"",
-							val),
-					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
-			*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
-			return false;
+			newline = (IdentLine *) lfirst(parsed_line_cell);
+			if (newline->ident_user[0] == '/')
+				pg_regfree(&newline->re);
 		}
+		MemoryContextDelete(ident_context);
+		return false;
+	}
 
-		foreach(l, parsed_ports)
+	/* Loaded new file successfully, replace the one we use */
+	if (parsed_ident_lines != NIL)
+	{
+		foreach(parsed_line_cell, parsed_ident_lines)
 		{
-			if (atoi(lfirst(l)) == 0)
+			newline = (IdentLine *) lfirst(parsed_line_cell);
+			if (newline->ident_user[0] == '/')
+				pg_regfree(&newline->re);
+		}
+	}
+	if (parsed_ident_context != NULL)
+		MemoryContextDelete(parsed_ident_context);
+
+	parsed_ident_context = ident_context;
+	parsed_ident_lines = new_parsed_lines;
+
+	return true;
+}
+
+
+
+/*
+ *	Determine what authentication method should be used when accessing database
+ *	"database" from frontend "raddr", user "user".  Return the method and
+ *	an optional argument (stored in fields of *port), and STATUS_OK.
+ *
+ *	If the file does not contain any entry matching the request, we return
+ *	method = uaImplicitReject.
+ */
+void
+hba_getauthmethod(hbaPort *port)
+{
+	check_hba(port);
+}
+
+
+/*
+ * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
+ *
+ * The return value is statically allocated (see the UserAuthName array) and
+ * should not be freed.
+ */
+const char *
+hba_authname(UserAuth auth_method)
+{
+	/*
+	 * Make sure UserAuthName[] tracks additions to the UserAuth enum
+	 */
+	StaticAssertStmt(lengthof(UserAuthName) == USER_AUTH_LAST + 1,
+					 "UserAuthName[] must match the UserAuth enum");
+
+	return UserAuthName[auth_method];
+}
+
+/*
+ * Parse one tokenised line from the hba config file and store the result in a
+ * HbaLine structure.
+ *
+ * If parsing fails, log a message at ereport level elevel, store an error
+ * string in tok_line->err_msg, and return NULL.  (Some non-error conditions
+ * can also result in such messages.)
+ *
+ * Note: this function leaks memory when an error occurs.  Caller is expected
+ * to have set a memory context that will be reset if this function returns
+ * NULL.
+ */
+HbaLine *
+parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
+{
+	int			line_num = tok_line->line_num;
+	char	  **err_msg = &tok_line->err_msg;
+	char	   *str;
+	struct addrinfo *gai_result;
+	struct addrinfo hints;
+	int			ret;
+	char	   *cidr_slash;
+	char	   *unsupauth;
+	ListCell   *field;
+	List	   *tokens;
+	ListCell   *tokencell;
+	HbaToken   *token;
+	HbaLine    *parsedline;
+
+	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->linenumber = line_num;
+	parsedline->rawline = pstrdup(tok_line->raw_line);
+
+	/* Check the record type. */
+	Assert(tok_line->fields != NIL);
+	field = list_head(tok_line->fields);
+	tokens = lfirst(field);
+	if (tokens->length > 1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("multiple values specified for connection type"),
+				 errhint("Specify exactly one connection type per line."),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = "multiple values specified for connection type";
+		return NULL;
+	}
+	token = linitial(tokens);
+	if (strcmp(token->string, "local") == 0)
+	{
+#ifdef HAVE_UNIX_SOCKETS
+		parsedline->conntype = ctLocal;
+#else
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("local connections are not supported by this build"),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = "local connections are not supported by this build";
+		return NULL;
+#endif
+	}
+	else if (strcmp(token->string, "host") == 0 ||
+			 strcmp(token->string, "hostssl") == 0 ||
+			 strcmp(token->string, "hostnossl") == 0 ||
+			 strcmp(token->string, "hostgssenc") == 0 ||
+			 strcmp(token->string, "hostnogssenc") == 0)
+	{
+
+		if (token->string[4] == 's')	/* "hostssl" */
+		{
+			parsedline->conntype = ctHostSSL;
+			/* Log a warning if SSL support is not active */
+#ifdef USE_SSL
+			if (!EnableSSL)
 			{
 				ereport(elevel,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
-						 errmsg("invalid RADIUS port number: \"%s\"", val),
+						 errmsg("hostssl record cannot match because SSL is disabled"),
+						 errhint("Set ssl = on in postgresql.conf."),
 						 errcontext("line %d of configuration file \"%s\"",
 									line_num, HbaFileName)));
-
-				return false;
+				*err_msg = "hostssl record cannot match because SSL is disabled";
 			}
-		}
-		hbaline->radiusports = parsed_ports;
-		hbaline->radiusports_s = pstrdup(val);
-	}
-	else if (strcmp(name, "radiussecrets") == 0)
-	{
-		List	   *parsed_secrets;
-		char	   *dupval = pstrdup(val);
-
-		REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecrets", "radius");
-
-		if (!SplitGUCList(dupval, ',', &parsed_secrets))
-		{
-			/* syntax error in list */
+#else
 			ereport(elevel,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("could not parse RADIUS secret list \"%s\"",
-							val),
+					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
+					 errhint("Compile with --with-ssl to use SSL connections."),
 					 errcontext("line %d of configuration file \"%s\"",
 								line_num, HbaFileName)));
-			return false;
+			*err_msg = "hostssl record cannot match because SSL is not supported by this build";
+#endif
 		}
-
-		hbaline->radiussecrets = parsed_secrets;
-		hbaline->radiussecrets_s = pstrdup(val);
-	}
-	else if (strcmp(name, "radiusidentifiers") == 0)
-	{
-		List	   *parsed_identifiers;
-		char	   *dupval = pstrdup(val);
-
-		REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifiers", "radius");
-
-		if (!SplitGUCList(dupval, ',', &parsed_identifiers))
+		else if (token->string[4] == 'g')	/* "hostgssenc" */
 		{
-			/* syntax error in list */
+			parsedline->conntype = ctHostGSS;
+#ifndef ENABLE_GSS
 			ereport(elevel,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
-					 errmsg("could not parse RADIUS identifiers list \"%s\"",
-							val),
+					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
+					 errhint("Compile with --with-gssapi to use GSSAPI connections."),
 					 errcontext("line %d of configuration file \"%s\"",
 								line_num, HbaFileName)));
-			return false;
+			*err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
+#endif
 		}
-
-		hbaline->radiusidentifiers = parsed_identifiers;
-		hbaline->radiusidentifiers_s = pstrdup(val);
-	}
+		else if (token->string[4] == 'n' && token->string[6] == 's')
+			parsedline->conntype = ctHostNoSSL;
+		else if (token->string[4] == 'n' && token->string[6] == 'g')
+			parsedline->conntype = ctHostNoGSS;
+		else
+		{
+			/* "host" */
+			parsedline->conntype = ctHost;
+		}
+	}							/* record type */
 	else
 	{
 		ereport(elevel,
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("unrecognized authentication option name: \"%s\"",
-						name),
+				 errmsg("invalid connection type \"%s\"",
+						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
 							line_num, HbaFileName)));
-		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
-							name);
-		return false;
+		*err_msg = psprintf("invalid connection type \"%s\"", token->string);
+		return NULL;
 	}
-	return true;
-}
-
-/*
- *	Scan the pre-parsed hba file, looking for a match to the port's connection
- *	request.
- */
-static void
-check_hba(hbaPort *port)
-{
-	Oid			roleid;
-	ListCell   *line;
-	HbaLine    *hba;
-
-	/* Get the target role's OID.  Note we do not error out for bad role. */
-	roleid = get_role_oid(port->user_name, true);
 
-	foreach(line, parsed_hba_lines)
+	/* Get the databases. */
+	field = lnext(tok_line->fields, field);
+	if (!field)
 	{
-		hba = (HbaLine *) lfirst(line);
-
-		/* Check connection type */
-		if (hba->conntype == ctLocal)
-		{
-			if (port->raddr.addr.ss_family != AF_UNIX)
-				continue;
-		}
-		else
-		{
-			if (port->raddr.addr.ss_family == AF_UNIX)
-				continue;
-
-			/* Check SSL state */
-			if (port->ssl_in_use)
-			{
-				/* Connection is SSL, match both "host" and "hostssl" */
-				if (hba->conntype == ctHostNoSSL)
-					continue;
-			}
-			else
-			{
-				/* Connection is not SSL, match both "host" and "hostnossl" */
-				if (hba->conntype == ctHostSSL)
-					continue;
-			}
-
-			/* Check GSSAPI state */
-#ifdef ENABLE_GSS
-			if (port->gss && port->gss->enc &&
-				hba->conntype == ctHostNoGSS)
-				continue;
-			else if (!(port->gss && port->gss->enc) &&
-					 hba->conntype == ctHostGSS)
-				continue;
-#else
-			if (hba->conntype == ctHostGSS)
-				continue;
-#endif
-
-			/* Check IP address */
-			switch (hba->ip_cmp_method)
-			{
-				case ipCmpMask:
-					if (hba->hostname)
-					{
-						if (!check_hostname(port,
-											hba->hostname))
-							continue;
-					}
-					else
-					{
-						if (!check_ip(&port->raddr,
-									  (struct sockaddr *) &hba->addr,
-									  (struct sockaddr *) &hba->mask))
-							continue;
-					}
-					break;
-				case ipCmpAll:
-					break;
-				case ipCmpSameHost:
-				case ipCmpSameNet:
-					if (!check_same_host_or_net(&port->raddr,
-												hba->ip_cmp_method))
-						continue;
-					break;
-				default:
-					/* shouldn't get here, but deem it no-match if so */
-					continue;
-			}
-		}						/* != ctLocal */
-
-		/* Check database and role */
-		if (!check_db(port->database_name, port->user_name, roleid,
-					  hba->databases))
-			continue;
-
-		if (!check_role(port->user_name, roleid, hba->roles))
-			continue;
-
-		/* Found a record that matched! */
-		port->hba = hba;
-		return;
-	}
-
-	/* If no matching entry was found, then implicitly reject. */
-	hba = palloc0(sizeof(HbaLine));
-	hba->auth_method = uaImplicitReject;
-	port->hba = hba;
-}
-
-/*
- * Read the config file and create a List of HbaLine records for the contents.
- *
- * The configuration is read into a temporary list, and if any parse error
- * occurs the old list is kept in place and false is returned.  Only if the
- * whole file parses OK is the list replaced, and the function returns true.
- *
- * On a false result, caller will take care of reporting a FATAL error in case
- * this is the initial startup.  If it happens on reload, we just keep running
- * with the old data.
- */
-bool
-load_hba(void)
-{
-	FILE	   *file;
-	List	   *hba_lines = NIL;
-	ListCell   *line;
-	List	   *new_parsed_lines = NIL;
-	bool		ok = true;
-	MemoryContext linecxt;
-	MemoryContext oldcxt;
-	MemoryContext hbacxt;
-
-	file = AllocateFile(HbaFileName, "r");
-	if (file == NULL)
-	{
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
-		return false;
-	}
-
-	linecxt = tokenize_file(HbaFileName, file, &hba_lines, LOG);
-	FreeFile(file);
-
-	/* Now parse all the lines */
-	Assert(PostmasterContext);
-	hbacxt = AllocSetContextCreate(PostmasterContext,
-								   "hba parser context",
-								   ALLOCSET_SMALL_SIZES);
-	oldcxt = MemoryContextSwitchTo(hbacxt);
-	foreach(line, hba_lines)
-	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line);
-		HbaLine    *newline;
-
-		/* don't parse lines that already have errors */
-		if (tok_line->err_msg != NULL)
-		{
-			ok = false;
-			continue;
-		}
-
-		if ((newline = parse_hba_line(tok_line, LOG)) == NULL)
-		{
-			/* Parse error; remember there's trouble */
-			ok = false;
-
-			/*
-			 * Keep parsing the rest of the file so we can report errors on
-			 * more than the first line.  Error has already been logged, no
-			 * need for more chatter here.
-			 */
-			continue;
-		}
-
-		new_parsed_lines = lappend(new_parsed_lines, newline);
-	}
-
-	/*
-	 * A valid HBA file must have at least one entry; else there's no way to
-	 * connect to the postmaster.  But only complain about this if we didn't
-	 * already have parsing errors.
-	 */
-	if (ok && new_parsed_lines == NIL)
-	{
-		ereport(LOG,
+		ereport(elevel,
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
-				 errmsg("configuration file \"%s\" contains no entries",
-						HbaFileName)));
-		ok = false;
+				 errmsg("end-of-line before database specification"),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = "end-of-line before database specification";
+		return NULL;
 	}
-
-	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
-	MemoryContextSwitchTo(oldcxt);
-
-	if (!ok)
+	parsedline->databases = NIL;
+	tokens = lfirst(field);
+	foreach(tokencell, tokens)
 	{
-		/* File contained one or more errors, so bail out */
-		MemoryContextDelete(hbacxt);
-		return false;
+		parsedline->databases = lappend(parsedline->databases,
+										copy_hba_token(lfirst(tokencell)));
 	}
 
-	/* Loaded new file successfully, replace the one we use */
-	if (parsed_hba_context != NULL)
-		MemoryContextDelete(parsed_hba_context);
-	parsed_hba_context = hbacxt;
-	parsed_hba_lines = new_parsed_lines;
-
-	return true;
-}
-
-/*
- * This macro specifies the maximum number of authentication options
- * that are possible with any given authentication method that is supported.
- * Currently LDAP supports 11, and there are 3 that are not dependent on
- * the auth method here.  It may not actually be possible to set all of them
- * at the same time, but we'll set the macro value high enough to be
- * conservative and avoid warnings from static analysis tools.
- */
-#define MAX_HBA_OPTIONS 14
-
-/*
- * Create a text array listing the options specified in the HBA line.
- * Return NULL if no options are specified.
- */
-static ArrayType *
-gethba_options(HbaLine *hba)
-{
-	int			noptions;
-	Datum		options[MAX_HBA_OPTIONS];
-
-	noptions = 0;
-
-	if (hba->auth_method == uaGSS || hba->auth_method == uaSSPI)
+	/* Get the roles. */
+	field = lnext(tok_line->fields, field);
+	if (!field)
 	{
-		if (hba->include_realm)
-			options[noptions++] =
-				CStringGetTextDatum("include_realm=true");
-
-		if (hba->krb_realm)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("krb_realm=%s", hba->krb_realm));
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("end-of-line before role specification"),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = "end-of-line before role specification";
+		return NULL;
 	}
-
-	if (hba->usermap)
-		options[noptions++] =
-			CStringGetTextDatum(psprintf("map=%s", hba->usermap));
-
-	if (hba->clientcert != clientCertOff)
-		options[noptions++] =
-			CStringGetTextDatum(psprintf("clientcert=%s", (hba->clientcert == clientCertCA) ? "verify-ca" : "verify-full"));
-
-	if (hba->pamservice)
-		options[noptions++] =
-			CStringGetTextDatum(psprintf("pamservice=%s", hba->pamservice));
-
-	if (hba->auth_method == uaLDAP)
+	parsedline->roles = NIL;
+	tokens = lfirst(field);
+	foreach(tokencell, tokens)
 	{
-		if (hba->ldapserver)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapserver=%s", hba->ldapserver));
-
-		if (hba->ldapport)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapport=%d", hba->ldapport));
-
-		if (hba->ldaptls)
-			options[noptions++] =
-				CStringGetTextDatum("ldaptls=true");
-
-		if (hba->ldapprefix)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapprefix=%s", hba->ldapprefix));
-
-		if (hba->ldapsuffix)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapsuffix=%s", hba->ldapsuffix));
-
-		if (hba->ldapbasedn)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapbasedn=%s", hba->ldapbasedn));
-
-		if (hba->ldapbinddn)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapbinddn=%s", hba->ldapbinddn));
-
-		if (hba->ldapbindpasswd)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapbindpasswd=%s",
-											 hba->ldapbindpasswd));
-
-		if (hba->ldapsearchattribute)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
-											 hba->ldapsearchattribute));
-
-		if (hba->ldapsearchfilter)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
-											 hba->ldapsearchfilter));
-
-		if (hba->ldapscope)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
+		parsedline->roles = lappend(parsedline->roles,
+									copy_hba_token(lfirst(tokencell)));
 	}
 
-	if (hba->auth_method == uaRADIUS)
+	if (parsedline->conntype != ctLocal)
 	{
-		if (hba->radiusservers_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiusservers=%s", hba->radiusservers_s));
-
-		if (hba->radiussecrets_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiussecrets=%s", hba->radiussecrets_s));
-
-		if (hba->radiusidentifiers_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiusidentifiers=%s", hba->radiusidentifiers_s));
+		/* Read the IP address field. (with or without CIDR netmask) */
+		field = lnext(tok_line->fields, field);
+		if (!field)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("end-of-line before IP address specification"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "end-of-line before IP address specification";
+			return NULL;
+		}
+		tokens = lfirst(field);
+		if (tokens->length > 1)
+		{
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("multiple values specified for host address"),
+					 errhint("Specify one address range per line."),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "multiple values specified for host address";
+			return NULL;
+		}
+		token = linitial(tokens);
 
-		if (hba->radiusports_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s));
-	}
+		if (token_is_keyword(token, "all"))
+		{
+			parsedline->ip_cmp_method = ipCmpAll;
+		}
+		else if (token_is_keyword(token, "samehost"))
+		{
+			/* Any IP on this host is allowed to connect */
+			parsedline->ip_cmp_method = ipCmpSameHost;
+		}
+		else if (token_is_keyword(token, "samenet"))
+		{
+			/* Any IP on the host's subnets is allowed to connect */
+			parsedline->ip_cmp_method = ipCmpSameNet;
+		}
+		else
+		{
+			/* IP and netmask are specified */
+			parsedline->ip_cmp_method = ipCmpMask;
 
-	/* If you add more options, consider increasing MAX_HBA_OPTIONS. */
-	Assert(noptions <= MAX_HBA_OPTIONS);
+			/* need a modifiable copy of token */
+			str = pstrdup(token->string);
 
-	if (noptions > 0)
-		return construct_array(options, noptions, TEXTOID, -1, false, TYPALIGN_INT);
-	else
-		return NULL;
-}
+			/* Check if it has a CIDR suffix and if so isolate it */
+			cidr_slash = strchr(str, '/');
+			if (cidr_slash)
+				*cidr_slash = '\0';
 
-/* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+			/* Get the IP address either way */
+			hints.ai_flags = AI_NUMERICHOST;
+			hints.ai_family = AF_UNSPEC;
+			hints.ai_socktype = 0;
+			hints.ai_protocol = 0;
+			hints.ai_addrlen = 0;
+			hints.ai_canonname = NULL;
+			hints.ai_addr = NULL;
+			hints.ai_next = NULL;
 
-/*
- * fill_hba_line: build one row of pg_hba_file_rules view, add it to tuplestore
- *
- * tuple_store: where to store data
- * tupdesc: tuple descriptor for the view
- * lineno: pg_hba.conf line number (must always be valid)
- * hba: parsed line data (can be NULL, in which case err_msg should be set)
- * err_msg: error message (NULL if none)
- *
- * Note: leaks memory, but we don't care since this is run in a short-lived
- * memory context.
- */
-static void
-fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
-{
-	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
-	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
-	char		buffer[NI_MAXHOST];
-	HeapTuple	tuple;
-	int			index;
-	ListCell   *lc;
-	const char *typestr;
-	const char *addrstr;
-	const char *maskstr;
-	ArrayType  *options;
-
-	Assert(tupdesc->natts == NUM_PG_HBA_FILE_RULES_ATTS);
-
-	memset(values, 0, sizeof(values));
-	memset(nulls, 0, sizeof(nulls));
-	index = 0;
-
-	/* line_number */
-	values[index++] = Int32GetDatum(lineno);
-
-	if (hba != NULL)
-	{
-		/* type */
-		/* Avoid a default: case so compiler will warn about missing cases */
-		typestr = NULL;
-		switch (hba->conntype)
-		{
-			case ctLocal:
-				typestr = "local";
-				break;
-			case ctHost:
-				typestr = "host";
-				break;
-			case ctHostSSL:
-				typestr = "hostssl";
-				break;
-			case ctHostNoSSL:
-				typestr = "hostnossl";
-				break;
-			case ctHostGSS:
-				typestr = "hostgssenc";
-				break;
-			case ctHostNoGSS:
-				typestr = "hostnogssenc";
-				break;
-		}
-		if (typestr)
-			values[index++] = CStringGetTextDatum(typestr);
-		else
-			nulls[index++] = true;
+			ret = pg_getaddrinfo_all(str, NULL, &hints, &gai_result);
+			if (ret == 0 && gai_result)
+			{
+				memcpy(&parsedline->addr, gai_result->ai_addr,
+					   gai_result->ai_addrlen);
+				parsedline->addrlen = gai_result->ai_addrlen;
+			}
+			else if (ret == EAI_NONAME)
+				parsedline->hostname = str;
+			else
+			{
+				ereport(elevel,
+						(errcode(ERRCODE_CONFIG_FILE_ERROR),
+						 errmsg("invalid IP address \"%s\": %s",
+								str, gai_strerror(ret)),
+						 errcontext("line %d of configuration file \"%s\"",
+									line_num, HbaFileName)));
+				*err_msg = psprintf("invalid IP address \"%s\": %s",
+									str, gai_strerror(ret));
+				if (gai_result)
+					pg_freeaddrinfo_all(hints.ai_family, gai_result);
+				return NULL;
+			}
 
-		/* database */
-		if (hba->databases)
-		{
-			/*
-			 * Flatten HbaToken list to string list.  It might seem that we
-			 * should re-quote any quoted tokens, but that has been rejected
-			 * on the grounds that it makes it harder to compare the array
-			 * elements to other system catalogs.  That makes entries like
-			 * "all" or "samerole" formally ambiguous ... but users who name
-			 * databases/roles that way are inflicting their own pain.
-			 */
-			List	   *names = NIL;
+			pg_freeaddrinfo_all(hints.ai_family, gai_result);
 
-			foreach(lc, hba->databases)
+			/* Get the netmask */
+			if (cidr_slash)
 			{
-				HbaToken   *tok = lfirst(lc);
+				if (parsedline->hostname)
+				{
+					ereport(elevel,
+							(errcode(ERRCODE_CONFIG_FILE_ERROR),
+							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
+									token->string),
+							 errcontext("line %d of configuration file \"%s\"",
+										line_num, HbaFileName)));
+					*err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
+										token->string);
+					return NULL;
+				}
 
-				names = lappend(names, tok->string);
+				if (pg_sockaddr_cidr_mask(&parsedline->mask, cidr_slash + 1,
+										  parsedline->addr.ss_family) < 0)
+				{
+					ereport(elevel,
+							(errcode(ERRCODE_CONFIG_FILE_ERROR),
+							 errmsg("invalid CIDR mask in address \"%s\"",
+									token->string),
+							 errcontext("line %d of configuration file \"%s\"",
+										line_num, HbaFileName)));
+					*err_msg = psprintf("invalid CIDR mask in address \"%s\"",
+										token->string);
+					return NULL;
+				}
+				parsedline->masklen = parsedline->addrlen;
+				pfree(str);
 			}
-			values[index++] = PointerGetDatum(strlist_to_textarray(names));
-		}
-		else
-			nulls[index++] = true;
+			else if (!parsedline->hostname)
+			{
+				/* Read the mask field. */
+				pfree(str);
+				field = lnext(tok_line->fields, field);
+				if (!field)
+				{
+					ereport(elevel,
+							(errcode(ERRCODE_CONFIG_FILE_ERROR),
+							 errmsg("end-of-line before netmask specification"),
+							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
+							 errcontext("line %d of configuration file \"%s\"",
+										line_num, HbaFileName)));
+					*err_msg = "end-of-line before netmask specification";
+					return NULL;
+				}
+				tokens = lfirst(field);
+				if (tokens->length > 1)
+				{
+					ereport(elevel,
+							(errcode(ERRCODE_CONFIG_FILE_ERROR),
+							 errmsg("multiple values specified for netmask"),
+							 errcontext("line %d of configuration file \"%s\"",
+										line_num, HbaFileName)));
+					*err_msg = "multiple values specified for netmask";
+					return NULL;
+				}
+				token = linitial(tokens);
 
-		/* user */
-		if (hba->roles)
-		{
-			/* Flatten HbaToken list to string list; see comment above */
-			List	   *roles = NIL;
+				ret = pg_getaddrinfo_all(token->string, NULL,
+										 &hints, &gai_result);
+				if (ret || !gai_result)
+				{
+					ereport(elevel,
+							(errcode(ERRCODE_CONFIG_FILE_ERROR),
+							 errmsg("invalid IP mask \"%s\": %s",
+									token->string, gai_strerror(ret)),
+							 errcontext("line %d of configuration file \"%s\"",
+										line_num, HbaFileName)));
+					*err_msg = psprintf("invalid IP mask \"%s\": %s",
+										token->string, gai_strerror(ret));
+					if (gai_result)
+						pg_freeaddrinfo_all(hints.ai_family, gai_result);
+					return NULL;
+				}
 
-			foreach(lc, hba->roles)
-			{
-				HbaToken   *tok = lfirst(lc);
+				memcpy(&parsedline->mask, gai_result->ai_addr,
+					   gai_result->ai_addrlen);
+				parsedline->masklen = gai_result->ai_addrlen;
+				pg_freeaddrinfo_all(hints.ai_family, gai_result);
 
-				roles = lappend(roles, tok->string);
+				if (parsedline->addr.ss_family != parsedline->mask.ss_family)
+				{
+					ereport(elevel,
+							(errcode(ERRCODE_CONFIG_FILE_ERROR),
+							 errmsg("IP address and mask do not match"),
+							 errcontext("line %d of configuration file \"%s\"",
+										line_num, HbaFileName)));
+					*err_msg = "IP address and mask do not match";
+					return NULL;
+				}
 			}
-			values[index++] = PointerGetDatum(strlist_to_textarray(roles));
 		}
-		else
-			nulls[index++] = true;
+	}							/* != ctLocal */
 
-		/* address and netmask */
-		/* Avoid a default: case so compiler will warn about missing cases */
-		addrstr = maskstr = NULL;
-		switch (hba->ip_cmp_method)
+	/* Get the authentication method */
+	field = lnext(tok_line->fields, field);
+	if (!field)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("end-of-line before authentication method"),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = "end-of-line before authentication method";
+		return NULL;
+	}
+	tokens = lfirst(field);
+	if (tokens->length > 1)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("multiple values specified for authentication type"),
+				 errhint("Specify exactly one authentication type per line."),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = "multiple values specified for authentication type";
+		return NULL;
+	}
+	token = linitial(tokens);
+
+	unsupauth = NULL;
+	if (strcmp(token->string, "trust") == 0)
+		parsedline->auth_method = uaTrust;
+	else if (strcmp(token->string, "ident") == 0)
+		parsedline->auth_method = uaIdent;
+	else if (strcmp(token->string, "peer") == 0)
+		parsedline->auth_method = uaPeer;
+	else if (strcmp(token->string, "password") == 0)
+		parsedline->auth_method = uaPassword;
+	else if (strcmp(token->string, "gss") == 0)
+#ifdef ENABLE_GSS
+		parsedline->auth_method = uaGSS;
+#else
+		unsupauth = "gss";
+#endif
+	else if (strcmp(token->string, "sspi") == 0)
+#ifdef ENABLE_SSPI
+		parsedline->auth_method = uaSSPI;
+#else
+		unsupauth = "sspi";
+#endif
+	else if (strcmp(token->string, "reject") == 0)
+		parsedline->auth_method = uaReject;
+	else if (strcmp(token->string, "md5") == 0)
+	{
+		if (Db_user_namespace)
 		{
-			case ipCmpMask:
-				if (hba->hostname)
-				{
-					addrstr = hba->hostname;
-				}
-				else
-				{
-					/*
-					 * Note: if pg_getnameinfo_all fails, it'll set buffer to
-					 * "???", which we want to return.
-					 */
-					if (hba->addrlen > 0)
-					{
-						if (pg_getnameinfo_all(&hba->addr, hba->addrlen,
-											   buffer, sizeof(buffer),
-											   NULL, 0,
-											   NI_NUMERICHOST) == 0)
-							clean_ipv6_addr(hba->addr.ss_family, buffer);
-						addrstr = pstrdup(buffer);
-					}
-					if (hba->masklen > 0)
-					{
-						if (pg_getnameinfo_all(&hba->mask, hba->masklen,
-											   buffer, sizeof(buffer),
-											   NULL, 0,
-											   NI_NUMERICHOST) == 0)
-							clean_ipv6_addr(hba->mask.ss_family, buffer);
-						maskstr = pstrdup(buffer);
-					}
-				}
-				break;
-			case ipCmpAll:
-				addrstr = "all";
-				break;
-			case ipCmpSameHost:
-				addrstr = "samehost";
-				break;
-			case ipCmpSameNet:
-				addrstr = "samenet";
-				break;
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
+			return NULL;
 		}
-		if (addrstr)
-			values[index++] = CStringGetTextDatum(addrstr);
-		else
-			nulls[index++] = true;
-		if (maskstr)
-			values[index++] = CStringGetTextDatum(maskstr);
-		else
-			nulls[index++] = true;
-
-		/* auth_method */
-		values[index++] = CStringGetTextDatum(hba_authname(hba->auth_method));
-
-		/* options */
-		options = gethba_options(hba);
-		if (options)
-			values[index++] = PointerGetDatum(options);
-		else
-			nulls[index++] = true;
+		parsedline->auth_method = uaMD5;
 	}
+	else if (strcmp(token->string, "scram-sha-256") == 0)
+		parsedline->auth_method = uaSCRAM;
+	else if (strcmp(token->string, "pam") == 0)
+#ifdef USE_PAM
+		parsedline->auth_method = uaPAM;
+#else
+		unsupauth = "pam";
+#endif
+	else if (strcmp(token->string, "bsd") == 0)
+#ifdef USE_BSD_AUTH
+		parsedline->auth_method = uaBSD;
+#else
+		unsupauth = "bsd";
+#endif
+	else if (strcmp(token->string, "ldap") == 0)
+#ifdef USE_LDAP
+		parsedline->auth_method = uaLDAP;
+#else
+		unsupauth = "ldap";
+#endif
+	else if (strcmp(token->string, "cert") == 0)
+#ifdef USE_SSL
+		parsedline->auth_method = uaCert;
+#else
+		unsupauth = "cert";
+#endif
+	else if (strcmp(token->string, "radius") == 0)
+		parsedline->auth_method = uaRADIUS;
 	else
 	{
-		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("invalid authentication method \"%s\"",
+						token->string),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = psprintf("invalid authentication method \"%s\"",
+							token->string);
+		return NULL;
 	}
 
-	/* error */
-	if (err_msg)
-		values[NUM_PG_HBA_FILE_RULES_ATTS - 1] = CStringGetTextDatum(err_msg);
-	else
-		nulls[NUM_PG_HBA_FILE_RULES_ATTS - 1] = true;
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	tuplestore_puttuple(tuple_store, tuple);
-}
-
-/*
- * Read the pg_hba.conf file and fill the tuplestore with view records.
- */
-static void
-fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
-{
-	FILE	   *file;
-	List	   *hba_lines = NIL;
-	ListCell   *line;
-	MemoryContext linecxt;
-	MemoryContext hbacxt;
-	MemoryContext oldcxt;
+	if (unsupauth)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("invalid authentication method \"%s\": not supported by this build",
+						token->string),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
+							token->string);
+		return NULL;
+	}
 
 	/*
-	 * In the unlikely event that we can't open pg_hba.conf, we throw an
-	 * error, rather than trying to report it via some sort of view entry.
-	 * (Most other error conditions should result in a message in a view
-	 * entry.)
+	 * XXX: When using ident on local connections, change it to peer, for
+	 * backwards compatibility.
 	 */
-	file = AllocateFile(HbaFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
-
-	linecxt = tokenize_file(HbaFileName, file, &hba_lines, DEBUG3);
-	FreeFile(file);
+	if (parsedline->conntype == ctLocal &&
+		parsedline->auth_method == uaIdent)
+		parsedline->auth_method = uaPeer;
 
-	/* Now parse all the lines */
-	hbacxt = AllocSetContextCreate(CurrentMemoryContext,
-								   "hba parser context",
-								   ALLOCSET_SMALL_SIZES);
-	oldcxt = MemoryContextSwitchTo(hbacxt);
-	foreach(line, hba_lines)
+	/* Invalid authentication combinations */
+	if (parsedline->conntype == ctLocal &&
+		parsedline->auth_method == uaGSS)
 	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line);
-		HbaLine    *hbaline = NULL;
-
-		/* don't parse lines that already have errors */
-		if (tok_line->err_msg == NULL)
-			hbaline = parse_hba_line(tok_line, DEBUG3);
-
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("gssapi authentication is not supported on local sockets"),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = "gssapi authentication is not supported on local sockets";
+		return NULL;
 	}
 
-	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
-	/* Free parse_hba_line memory */
-	MemoryContextSwitchTo(oldcxt);
-	MemoryContextDelete(hbacxt);
-}
-
-/*
- * SQL-accessible SRF to return all the entries in the pg_hba.conf file.
- */
-Datum
-pg_hba_file_rules(PG_FUNCTION_ARGS)
-{
-	Tuplestorestate *tuple_store;
-	TupleDesc	tupdesc;
-	MemoryContext old_cxt;
-	ReturnSetInfo *rsi;
+	if (parsedline->conntype != ctLocal &&
+		parsedline->auth_method == uaPeer)
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("peer authentication is only supported on local sockets"),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = "peer authentication is only supported on local sockets";
+		return NULL;
+	}
 
 	/*
-	 * We must use the Materialize mode to be safe against HBA file changes
-	 * while the cursor is open. It's also more efficient than having to look
-	 * up our current position in the parsed list every time.
+	 * SSPI authentication can never be enabled on ctLocal connections,
+	 * because it's only supported on Windows, where ctLocal isn't supported.
 	 */
-	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
-
-	/* Check to see if caller supports us returning a tuplestore */
-	if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("set-valued function called in context that cannot accept a set")));
-	if (!(rsi->allowedModes & SFRM_Materialize))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("materialize mode required, but it is not allowed in this context")));
-
-	rsi->returnMode = SFRM_Materialize;
-
-	/* Build a tuple descriptor for our result type */
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
-	/* Build tuplestore to hold the result rows */
-	old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
-
-	tuple_store =
-		tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random,
-							  false, work_mem);
-	rsi->setDesc = tupdesc;
-	rsi->setResult = tuple_store;
-
-	MemoryContextSwitchTo(old_cxt);
-
-	/* Fill the tuplestore */
-	fill_hba_view(tuple_store, tupdesc);
-
-	PG_RETURN_NULL();
-}
-
-
-/*
- * Parse one tokenised line from the ident config file and store the result in
- * an IdentLine structure.
- *
- * If parsing fails, log a message and return NULL.
- *
- * If ident_user is a regular expression (ie. begins with a slash), it is
- * compiled and stored in IdentLine structure.
- *
- * Note: this function leaks memory when an error occurs.  Caller is expected
- * to have set a memory context that will be reset if this function returns
- * NULL.
- */
-static IdentLine *
-parse_ident_line(TokenizedLine *tok_line)
-{
-	int			line_num = tok_line->line_num;
-	ListCell   *field;
-	List	   *tokens;
-	HbaToken   *token;
-	IdentLine  *parsedline;
-
-	Assert(tok_line->fields != NIL);
-	field = list_head(tok_line->fields);
-
-	parsedline = palloc0(sizeof(IdentLine));
-	parsedline->linenumber = line_num;
-
-	/* Get the map token (must exist) */
-	tokens = lfirst(field);
-	IDENT_MULTI_VALUE(tokens);
-	token = linitial(tokens);
-	parsedline->usermap = pstrdup(token->string);
 
-	/* Get the ident user token */
-	field = lnext(tok_line->fields, field);
-	IDENT_FIELD_ABSENT(field);
-	tokens = lfirst(field);
-	IDENT_MULTI_VALUE(tokens);
-	token = linitial(tokens);
-	parsedline->ident_user = pstrdup(token->string);
-
-	/* Get the PG rolename token */
-	field = lnext(tok_line->fields, field);
-	IDENT_FIELD_ABSENT(field);
-	tokens = lfirst(field);
-	IDENT_MULTI_VALUE(tokens);
-	token = linitial(tokens);
-	parsedline->pg_role = pstrdup(token->string);
 
-	if (parsedline->ident_user[0] == '/')
+	if (parsedline->conntype != ctHostSSL &&
+		parsedline->auth_method == uaCert)
 	{
-		/*
-		 * When system username starts with a slash, treat it as a regular
-		 * expression. Pre-compile it.
-		 */
-		int			r;
-		pg_wchar   *wstr;
-		int			wlen;
-
-		wstr = palloc((strlen(parsedline->ident_user + 1) + 1) * sizeof(pg_wchar));
-		wlen = pg_mb2wchar_with_len(parsedline->ident_user + 1,
-									wstr, strlen(parsedline->ident_user + 1));
-
-		r = pg_regcomp(&parsedline->re, wstr, wlen, REG_ADVANCED, C_COLLATION_OID);
-		if (r)
-		{
-			char		errstr[100];
-
-			pg_regerror(r, &parsedline->re, errstr, sizeof(errstr));
-			ereport(LOG,
-					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
-					 errmsg("invalid regular expression \"%s\": %s",
-							parsedline->ident_user + 1, errstr)));
-
-			pfree(wstr);
-			return NULL;
-		}
-		pfree(wstr);
-	}
-
-	return parsedline;
-}
-
-/*
- *	Process one line from the parsed ident config lines.
- *
- *	Compare input parsed ident line to the needed map, pg_role and ident_user.
- *	*found_p and *error_p are set according to our results.
- */
-static void
-check_ident_usermap(IdentLine *identLine, const char *usermap_name,
-					const char *pg_role, const char *ident_user,
-					bool case_insensitive, bool *found_p, bool *error_p)
-{
-	*found_p = false;
-	*error_p = false;
+		ereport(elevel,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("cert authentication is only supported on hostssl connections"),
+				 errcontext("line %d of configuration file \"%s\"",
+							line_num, HbaFileName)));
+		*err_msg = "cert authentication is only supported on hostssl connections";
+		return NULL;
+	}
 
-	if (strcmp(identLine->usermap, usermap_name) != 0)
-		/* Line does not match the map name we're looking for, so just abort */
-		return;
+	/*
+	 * For GSS and SSPI, set the default value of include_realm to true.
+	 * Having include_realm set to false is dangerous in multi-realm
+	 * situations and is generally considered bad practice.  We keep the
+	 * capability around for backwards compatibility, but we might want to
+	 * remove it at some point in the future.  Users who still need to strip
+	 * the realm off would be better served by using an appropriate regex in a
+	 * pg_ident.conf mapping.
+	 */
+	if (parsedline->auth_method == uaGSS ||
+		parsedline->auth_method == uaSSPI)
+		parsedline->include_realm = true;
 
-	/* Match? */
-	if (identLine->ident_user[0] == '/')
+	/*
+	 * For SSPI, include_realm defaults to the SAM-compatible domain (aka
+	 * NetBIOS name) and user names instead of the Kerberos principal name for
+	 * compatibility.
+	 */
+	if (parsedline->auth_method == uaSSPI)
 	{
-		/*
-		 * When system username starts with a slash, treat it as a regular
-		 * expression. In this case, we process the system username as a
-		 * regular expression that returns exactly one match. This is replaced
-		 * for \1 in the database username string, if present.
-		 */
-		int			r;
-		regmatch_t	matches[2];
-		pg_wchar   *wstr;
-		int			wlen;
-		char	   *ofs;
-		char	   *regexp_pgrole;
-
-		wstr = palloc((strlen(ident_user) + 1) * sizeof(pg_wchar));
-		wlen = pg_mb2wchar_with_len(ident_user, wstr, strlen(ident_user));
+		parsedline->compat_realm = true;
+		parsedline->upn_username = false;
+	}
 
-		r = pg_regexec(&identLine->re, wstr, wlen, 0, NULL, 2, matches, 0);
-		if (r)
+	/* Parse remaining arguments */
+	while ((field = lnext(tok_line->fields, field)) != NULL)
+	{
+		tokens = lfirst(field);
+		foreach(tokencell, tokens)
 		{
-			char		errstr[100];
+			char	   *val;
 
-			if (r != REG_NOMATCH)
+			token = lfirst(tokencell);
+
+			str = pstrdup(token->string);
+			val = strchr(str, '=');
+			if (val == NULL)
 			{
-				/* REG_NOMATCH is not an error, everything else is */
-				pg_regerror(r, &identLine->re, errstr, sizeof(errstr));
-				ereport(LOG,
-						(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
-						 errmsg("regular expression match for \"%s\" failed: %s",
-								identLine->ident_user + 1, errstr)));
-				*error_p = true;
+				/*
+				 * Got something that's not a name=value pair.
+				 */
+				ereport(elevel,
+						(errcode(ERRCODE_CONFIG_FILE_ERROR),
+						 errmsg("authentication option not in name=value format: %s", token->string),
+						 errcontext("line %d of configuration file \"%s\"",
+									line_num, HbaFileName)));
+				*err_msg = psprintf("authentication option not in name=value format: %s",
+									token->string);
+				return NULL;
 			}
 
-			pfree(wstr);
-			return;
+			*val++ = '\0';		/* str now holds "name", val holds "value" */
+			if (!parse_hba_auth_opt(str, val, parsedline, elevel, err_msg))
+				/* parse_hba_auth_opt already logged the error message */
+				return NULL;
+			pfree(str);
 		}
-		pfree(wstr);
+	}
 
-		if ((ofs = strstr(identLine->pg_role, "\\1")) != NULL)
-		{
-			int			offset;
+	/*
+	 * Check if the selected authentication method has any mandatory arguments
+	 * that are not set.
+	 */
+	if (parsedline->auth_method == uaLDAP)
+	{
+#ifndef HAVE_LDAP_INITIALIZE
+		/* Not mandatory for OpenLDAP, because it can use DNS SRV records */
+		MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap");
+#endif
 
-			/* substitution of the first argument requested */
-			if (matches[1].rm_so < 0)
+		/*
+		 * 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 one of
+		 * ldapsearchattribute or ldapsearchfilter.  Disallow mixing these
+		 * parameters.
+		 */
+		if (parsedline->ldapprefix || parsedline->ldapsuffix)
+		{
+			if (parsedline->ldapbasedn ||
+				parsedline->ldapbinddn ||
+				parsedline->ldapbindpasswd ||
+				parsedline->ldapsearchattribute ||
+				parsedline->ldapsearchfilter)
 			{
-				ereport(LOG,
-						(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
-						 errmsg("regular expression \"%s\" has no subexpressions as requested by backreference in \"%s\"",
-								identLine->ident_user + 1, identLine->pg_role)));
-				*error_p = true;
-				return;
+				ereport(elevel,
+						(errcode(ERRCODE_CONFIG_FILE_ERROR),
+						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
+						 errcontext("line %d of configuration file \"%s\"",
+									line_num, HbaFileName)));
+				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
+				return NULL;
 			}
-
-			/*
-			 * 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);
-			memcpy(regexp_pgrole + offset,
-				   ident_user + matches[1].rm_so,
-				   matches[1].rm_eo - matches[1].rm_so);
-			strcat(regexp_pgrole, ofs + 2);
 		}
-		else
+		else if (!parsedline->ldapbasedn)
 		{
-			/* no substitution, so copy the match */
-			regexp_pgrole = pstrdup(identLine->pg_role);
+			ereport(elevel,
+					(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)));
+			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
+			return NULL;
 		}
 
 		/*
-		 * now check if the username actually matched what the user is trying
-		 * to connect as
+		 * When using search+bind, you can either use a simple attribute
+		 * (defaulting to "uid") or a fully custom search filter.  You can't
+		 * do both.
 		 */
-		if (case_insensitive)
-		{
-			if (pg_strcasecmp(regexp_pgrole, pg_role) == 0)
-				*found_p = true;
-		}
-		else
+		if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter)
 		{
-			if (strcmp(regexp_pgrole, pg_role) == 0)
-				*found_p = true;
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
+			return NULL;
 		}
-		pfree(regexp_pgrole);
-
-		return;
 	}
-	else
+
+	if (parsedline->auth_method == uaRADIUS)
 	{
-		/* Not regular expression, so make complete match */
-		if (case_insensitive)
+		MANDATORY_AUTH_ARG(parsedline->radiusservers, "radiusservers", "radius");
+		MANDATORY_AUTH_ARG(parsedline->radiussecrets, "radiussecrets", "radius");
+
+		if (list_length(parsedline->radiusservers) < 1)
 		{
-			if (pg_strcasecmp(identLine->pg_role, pg_role) == 0 &&
-				pg_strcasecmp(identLine->ident_user, ident_user) == 0)
-				*found_p = true;
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("list of RADIUS servers cannot be empty"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "list of RADIUS servers cannot be empty";
+			return NULL;
 		}
-		else
+
+		if (list_length(parsedline->radiussecrets) < 1)
 		{
-			if (strcmp(identLine->pg_role, pg_role) == 0 &&
-				strcmp(identLine->ident_user, ident_user) == 0)
-				*found_p = true;
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("list of RADIUS secrets cannot be empty"),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = "list of RADIUS secrets cannot be empty";
+			return NULL;
 		}
-	}
-}
-
-
-/*
- *	Scan the (pre-parsed) ident usermap file line by line, looking for a match
- *
- *	See if the user with ident username "auth_user" is allowed to act
- *	as Postgres user "pg_role" according to usermap "usermap_name".
- *
- *	Special case: Usermap NULL, equivalent to what was previously called
- *	"sameuser" or "samerole", means don't look in the usermap file.
- *	That's an implied map wherein "pg_role" must be identical to
- *	"auth_user" in order to be authorized.
- *
- *	Iff authorized, return STATUS_OK, otherwise return STATUS_ERROR.
- */
-int
-check_usermap(const char *usermap_name,
-			  const char *pg_role,
-			  const char *auth_user,
-			  bool case_insensitive)
-{
-	bool		found_entry = false,
-				error = false;
 
-	if (usermap_name == NULL || usermap_name[0] == '\0')
-	{
-		if (case_insensitive)
+		/*
+		 * Verify length of option lists - each can be 0 (except for secrets,
+		 * but that's already checked above), 1 (use the same value
+		 * everywhere) or the same as the number of servers.
+		 */
+		if (!(list_length(parsedline->radiussecrets) == 1 ||
+			  list_length(parsedline->radiussecrets) == list_length(parsedline->radiusservers)))
 		{
-			if (pg_strcasecmp(pg_role, auth_user) == 0)
-				return STATUS_OK;
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
+							list_length(parsedline->radiussecrets),
+							list_length(parsedline->radiusservers)),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
+								list_length(parsedline->radiussecrets),
+								list_length(parsedline->radiusservers));
+			return NULL;
 		}
-		else
+		if (!(list_length(parsedline->radiusports) == 0 ||
+			  list_length(parsedline->radiusports) == 1 ||
+			  list_length(parsedline->radiusports) == list_length(parsedline->radiusservers)))
 		{
-			if (strcmp(pg_role, auth_user) == 0)
-				return STATUS_OK;
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
+							list_length(parsedline->radiusports),
+							list_length(parsedline->radiusservers)),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
+								list_length(parsedline->radiusports),
+								list_length(parsedline->radiusservers));
+			return NULL;
 		}
-		ereport(LOG,
-				(errmsg("provided user name (%s) and authenticated user name (%s) do not match",
-						pg_role, auth_user)));
-		return STATUS_ERROR;
-	}
-	else
-	{
-		ListCell   *line_cell;
-
-		foreach(line_cell, parsed_ident_lines)
+		if (!(list_length(parsedline->radiusidentifiers) == 0 ||
+			  list_length(parsedline->radiusidentifiers) == 1 ||
+			  list_length(parsedline->radiusidentifiers) == list_length(parsedline->radiusservers)))
 		{
-			check_ident_usermap(lfirst(line_cell), usermap_name,
-								pg_role, auth_user, case_insensitive,
-								&found_entry, &error);
-			if (found_entry || error)
-				break;
+			ereport(elevel,
+					(errcode(ERRCODE_CONFIG_FILE_ERROR),
+					 errmsg("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
+							list_length(parsedline->radiusidentifiers),
+							list_length(parsedline->radiusservers)),
+					 errcontext("line %d of configuration file \"%s\"",
+								line_num, HbaFileName)));
+			*err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
+								list_length(parsedline->radiusidentifiers),
+								list_length(parsedline->radiusservers));
+			return NULL;
 		}
 	}
-	if (!found_entry && !error)
+
+	/*
+	 * Enforce any parameters implied by other settings.
+	 */
+	if (parsedline->auth_method == uaCert)
 	{
-		ereport(LOG,
-				(errmsg("no match in usermap \"%s\" for user \"%s\" authenticated as \"%s\"",
-						usermap_name, pg_role, auth_user)));
+		/*
+		 * For auth method cert, client certificate validation is mandatory, and it implies
+		 * the level of verify-full.
+		 */
+		parsedline->clientcert = clientCertFull;
 	}
-	return found_entry ? STATUS_OK : STATUS_ERROR;
-}
 
+	return parsedline;
+}
 
 /*
- * Read the ident config file and create a List of IdentLine records for
- * the contents.
+ * Tokenize the given file.
  *
- * This works the same as load_hba(), but for the user config file.
+ * The output is a list of TokenizedAuthLine structs; see struct definition
+ * above.
+ *
+ * filename: the absolute path to the target file
+ * file: the already-opened target file
+ * tok_lines: receives output list
+ * elevel: message logging level
+ *
+ * Errors are reported by logging messages at ereport level elevel and by
+ * adding TokenizedAuthLine structs containing non-null err_msg fields to the
+ * output list.
+ *
+ * Return value is a memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
  */
-bool
-load_ident(void)
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, int elevel)
 {
-	FILE	   *file;
-	List	   *ident_lines = NIL;
-	ListCell   *line_cell,
-			   *parsed_line_cell;
-	List	   *new_parsed_lines = NIL;
-	bool		ok = true;
+	int			line_number = 1;
+	StringInfoData buf;
 	MemoryContext linecxt;
 	MemoryContext oldcxt;
-	MemoryContext ident_context;
-	IdentLine  *newline;
 
-	file = AllocateFile(IdentFileName, "r");
-	if (file == NULL)
-	{
-		/* not fatal ... we just won't do any special ident maps */
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open usermap file \"%s\": %m",
-						IdentFileName)));
-		return false;
-	}
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(linecxt);
 
-	linecxt = tokenize_file(IdentFileName, file, &ident_lines, LOG);
-	FreeFile(file);
+	initStringInfo(&buf);
 
-	/* Now parse all the lines */
-	Assert(PostmasterContext);
-	ident_context = AllocSetContextCreate(PostmasterContext,
-										  "ident parser context",
-										  ALLOCSET_SMALL_SIZES);
-	oldcxt = MemoryContextSwitchTo(ident_context);
-	foreach(line_cell, ident_lines)
+	*tok_lines = NIL;
+
+	while (!feof(file) && !ferror(file))
 	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line_cell);
+		char	   *lineptr;
+		List	   *current_line = NIL;
+		char	   *err_msg = NULL;
+		int			last_backslash_buflen = 0;
+		int			continuations = 0;
 
-		/* don't parse lines that already have errors */
-		if (tok_line->err_msg != NULL)
-		{
-			ok = false;
-			continue;
-		}
+		/* Collect the next input line, handling backslash continuations */
+		resetStringInfo(&buf);
 
-		if ((newline = parse_ident_line(tok_line)) == NULL)
+		while (pg_get_line_append(file, &buf, NULL))
 		{
-			/* Parse error; remember there's trouble */
-			ok = false;
+			/* Strip trailing newline, including \r in case we're on Windows */
+			buf.len = pg_strip_crlf(buf.data);
 
 			/*
-			 * Keep parsing the rest of the file so we can report errors on
-			 * more than the first line.  Error has already been logged, no
-			 * need for more chatter here.
+			 * Check for backslash continuation.  The backslash must be after
+			 * the last place we found a continuation, else two backslashes
+			 * followed by two \n's would behave surprisingly.
 			 */
-			continue;
-		}
-
-		new_parsed_lines = lappend(new_parsed_lines, newline);
-	}
-
-	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
-	MemoryContextSwitchTo(oldcxt);
+			if (buf.len > last_backslash_buflen &&
+				buf.data[buf.len - 1] == '\\')
+			{
+				/* Continuation, so strip it and keep reading */
+				buf.data[--buf.len] = '\0';
+				last_backslash_buflen = buf.len;
+				continuations++;
+				continue;
+			}
 
-	if (!ok)
-	{
-		/*
-		 * File contained one or more errors, so bail out, first being careful
-		 * to clean up whatever we allocated.  Most stuff will go away via
-		 * MemoryContextDelete, but we have to clean up regexes explicitly.
-		 */
-		foreach(parsed_line_cell, new_parsed_lines)
-		{
-			newline = (IdentLine *) lfirst(parsed_line_cell);
-			if (newline->ident_user[0] == '/')
-				pg_regfree(&newline->re);
+			/* Nope, so we have the whole line */
+			break;
 		}
-		MemoryContextDelete(ident_context);
-		return false;
-	}
 
-	/* Loaded new file successfully, replace the one we use */
-	if (parsed_ident_lines != NIL)
-	{
-		foreach(parsed_line_cell, parsed_ident_lines)
+		if (ferror(file))
 		{
-			newline = (IdentLine *) lfirst(parsed_line_cell);
-			if (newline->ident_user[0] == '/')
-				pg_regfree(&newline->re);
-		}
-	}
-	if (parsed_ident_context != NULL)
-		MemoryContextDelete(parsed_ident_context);
+			/* I/O error! */
+			int			save_errno = errno;
 
-	parsed_ident_context = ident_context;
-	parsed_ident_lines = new_parsed_lines;
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg("could not read file \"%s\": %m", filename)));
+			err_msg = psprintf("could not read file \"%s\": %s",
+							   filename, strerror(save_errno));
+			break;
+		}
 
-	return true;
-}
+		/* Parse fields */
+		lineptr = buf.data;
+		while (*lineptr && err_msg == NULL)
+		{
+			List	   *current_field;
 
+			current_field = next_field_expand(filename, &lineptr,
+											  elevel, &err_msg);
+			/* add field to line, unless we are at EOL or comment start */
+			if (current_field != NIL)
+				current_line = lappend(current_line, current_field);
+		}
 
+		/*
+		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * */
+		if (current_line != NIL || err_msg != NULL)
+		{
+			TokenizedAuthLine *tok_line;
 
-/*
- *	Determine what authentication method should be used when accessing database
- *	"database" from frontend "raddr", user "user".  Return the method and
- *	an optional argument (stored in fields of *port), and STATUS_OK.
- *
- *	If the file does not contain any entry matching the request, we return
- *	method = uaImplicitReject.
- */
-void
-hba_getauthmethod(hbaPort *port)
-{
-	check_hba(port);
-}
+			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+			tok_line->fields = current_line;
+			tok_line->line_num = line_number;
+			tok_line->raw_line = pstrdup(buf.data);
+			tok_line->err_msg = err_msg;
+			*tok_lines = lappend(*tok_lines, tok_line);
+		}
 
+		line_number += continuations + 1;
+	}
 
-/*
- * Return the name of the auth method in use ("gss", "md5", "trust", etc.).
- *
- * The return value is statically allocated (see the UserAuthName array) and
- * should not be freed.
- */
-const char *
-hba_authname(UserAuth auth_method)
-{
-	/*
-	 * Make sure UserAuthName[] tracks additions to the UserAuth enum
-	 */
-	StaticAssertStmt(lengthof(UserAuthName) == USER_AUTH_LAST + 1,
-					 "UserAuthName[] must match the UserAuth enum");
+	MemoryContextSwitchTo(oldcxt);
 
-	return UserAuthName[auth_method];
+	return linecxt;
 }
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 41b486bcef..7c722ea2ce 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -42,6 +42,7 @@ OBJS = \
 	geo_ops.o \
 	geo_selfuncs.o \
 	geo_spgist.o \
+	hbafuncs.o \
 	inet_cidr_ntop.o \
 	inet_net_pton.o \
 	int.o \
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
new file mode 100644
index 0000000000..2bcaebe99e
--- /dev/null
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -0,0 +1,452 @@
+/*-------------------------------------------------------------------------
+ *
+ * hbafuncs.c
+ *	  Support functions for authentication files SQL views.
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/hbafuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "catalog/objectaddress.h"
+#include "common/ip.h"
+#include "funcapi.h"
+#include "libpq/hba.h"
+#include "miscadmin.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+//#include "utils/tuplestore.h"
+
+
+static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+						  int lineno, HbaLine *hba, const char *err_msg);
+static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+static ArrayType *gethba_options(HbaLine *hba);
+
+
+/* Number of columns in pg_hba_file_rules view */
+#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+
+/*
+ * fill_hba_line: build one row of pg_hba_file_rules view, add it to tuplestore
+ *
+ * tuple_store: where to store data
+ * tupdesc: tuple descriptor for the view
+ * lineno: pg_hba.conf line number (must always be valid)
+ * hba: parsed line data (can be NULL, in which case err_msg should be set)
+ * err_msg: error message (NULL if none)
+ *
+ * Note: leaks memory, but we don't care since this is run in a short-lived
+ * memory context.
+ */
+static void
+fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+			  int lineno, HbaLine *hba, const char *err_msg)
+{
+	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
+	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
+	char		buffer[NI_MAXHOST];
+	HeapTuple	tuple;
+	int			index;
+	ListCell   *lc;
+	const char *typestr;
+	const char *addrstr;
+	const char *maskstr;
+	ArrayType  *options;
+
+	Assert(tupdesc->natts == NUM_PG_HBA_FILE_RULES_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (hba != NULL)
+	{
+		/* type */
+		/* Avoid a default: case so compiler will warn about missing cases */
+		typestr = NULL;
+		switch (hba->conntype)
+		{
+			case ctLocal:
+				typestr = "local";
+				break;
+			case ctHost:
+				typestr = "host";
+				break;
+			case ctHostSSL:
+				typestr = "hostssl";
+				break;
+			case ctHostNoSSL:
+				typestr = "hostnossl";
+				break;
+			case ctHostGSS:
+				typestr = "hostgssenc";
+				break;
+			case ctHostNoGSS:
+				typestr = "hostnogssenc";
+				break;
+		}
+		if (typestr)
+			values[index++] = CStringGetTextDatum(typestr);
+		else
+			nulls[index++] = true;
+
+		/* database */
+		if (hba->databases)
+		{
+			/*
+			 * Flatten HbaToken list to string list.  It might seem that we
+			 * should re-quote any quoted tokens, but that has been rejected
+			 * on the grounds that it makes it harder to compare the array
+			 * elements to other system catalogs.  That makes entries like
+			 * "all" or "samerole" formally ambiguous ... but users who name
+			 * databases/roles that way are inflicting their own pain.
+			 */
+			List	   *names = NIL;
+
+			foreach(lc, hba->databases)
+			{
+				HbaToken   *tok = lfirst(lc);
+
+				names = lappend(names, tok->string);
+			}
+			values[index++] = PointerGetDatum(strlist_to_textarray(names));
+		}
+		else
+			nulls[index++] = true;
+
+		/* user */
+		if (hba->roles)
+		{
+			/* Flatten HbaToken list to string list; see comment above */
+			List	   *roles = NIL;
+
+			foreach(lc, hba->roles)
+			{
+				HbaToken   *tok = lfirst(lc);
+
+				roles = lappend(roles, tok->string);
+			}
+			values[index++] = PointerGetDatum(strlist_to_textarray(roles));
+		}
+		else
+			nulls[index++] = true;
+
+		/* address and netmask */
+		/* Avoid a default: case so compiler will warn about missing cases */
+		addrstr = maskstr = NULL;
+		switch (hba->ip_cmp_method)
+		{
+			case ipCmpMask:
+				if (hba->hostname)
+				{
+					addrstr = hba->hostname;
+				}
+				else
+				{
+					/*
+					 * Note: if pg_getnameinfo_all fails, it'll set buffer to
+					 * "???", which we want to return.
+					 */
+					if (hba->addrlen > 0)
+					{
+						if (pg_getnameinfo_all(&hba->addr, hba->addrlen,
+											   buffer, sizeof(buffer),
+											   NULL, 0,
+											   NI_NUMERICHOST) == 0)
+							clean_ipv6_addr(hba->addr.ss_family, buffer);
+						addrstr = pstrdup(buffer);
+					}
+					if (hba->masklen > 0)
+					{
+						if (pg_getnameinfo_all(&hba->mask, hba->masklen,
+											   buffer, sizeof(buffer),
+											   NULL, 0,
+											   NI_NUMERICHOST) == 0)
+							clean_ipv6_addr(hba->mask.ss_family, buffer);
+						maskstr = pstrdup(buffer);
+					}
+				}
+				break;
+			case ipCmpAll:
+				addrstr = "all";
+				break;
+			case ipCmpSameHost:
+				addrstr = "samehost";
+				break;
+			case ipCmpSameNet:
+				addrstr = "samenet";
+				break;
+		}
+		if (addrstr)
+			values[index++] = CStringGetTextDatum(addrstr);
+		else
+			nulls[index++] = true;
+		if (maskstr)
+			values[index++] = CStringGetTextDatum(maskstr);
+		else
+			nulls[index++] = true;
+
+		/* auth_method */
+		values[index++] = CStringGetTextDatum(hba_authname(hba->auth_method));
+
+		/* options */
+		options = gethba_options(hba);
+		if (options)
+			values[index++] = PointerGetDatum(options);
+		else
+			nulls[index++] = true;
+	}
+	else
+	{
+		/* no parsing result, so set relevant fields to nulls */
+		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_HBA_FILE_RULES_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_HBA_FILE_RULES_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * Read the pg_hba.conf file and fill the tuplestore with view records.
+ */
+static void
+fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *hba_lines = NIL;
+	ListCell   *line;
+	MemoryContext linecxt;
+	MemoryContext hbacxt;
+	MemoryContext oldcxt;
+
+	/*
+	 * In the unlikely event that we can't open pg_hba.conf, we throw an
+	 * error, rather than trying to report it via some sort of view entry.
+	 * (Most other error conditions should result in a message in a view
+	 * entry.)
+	 */
+	file = AllocateFile(HbaFileName, "r");
+	if (file == NULL)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration file \"%s\": %m",
+						HbaFileName)));
+
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	FreeFile(file);
+
+	/* Now parse all the lines */
+	hbacxt = AllocSetContextCreate(CurrentMemoryContext,
+								   "hba parser context",
+								   ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hbacxt);
+	foreach(line, hba_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		HbaLine    *hbaline = NULL;
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg == NULL)
+			hbaline = parse_hba_line(tok_line, DEBUG3);
+
+		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
+					  hbaline, tok_line->err_msg);
+	}
+
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	/* Free parse_hba_line memory */
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(hbacxt);
+}
+
+/*
+ * This macro specifies the maximum number of authentication options
+ * that are possible with any given authentication method that is supported.
+ * Currently LDAP supports 11, and there are 3 that are not dependent on
+ * the auth method here.  It may not actually be possible to set all of them
+ * at the same time, but we'll set the macro value high enough to be
+ * conservative and avoid warnings from static analysis tools.
+ */
+#define MAX_HBA_OPTIONS 14
+
+/*
+ * Create a text array listing the options specified in the HBA line.
+ * Return NULL if no options are specified.
+ */
+static ArrayType *
+gethba_options(HbaLine *hba)
+{
+	int			noptions;
+	Datum		options[MAX_HBA_OPTIONS];
+
+	noptions = 0;
+
+	if (hba->auth_method == uaGSS || hba->auth_method == uaSSPI)
+	{
+		if (hba->include_realm)
+			options[noptions++] =
+				CStringGetTextDatum("include_realm=true");
+
+		if (hba->krb_realm)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("krb_realm=%s", hba->krb_realm));
+	}
+
+	if (hba->usermap)
+		options[noptions++] =
+			CStringGetTextDatum(psprintf("map=%s", hba->usermap));
+
+	if (hba->clientcert != clientCertOff)
+		options[noptions++] =
+			CStringGetTextDatum(psprintf("clientcert=%s", (hba->clientcert == clientCertCA) ? "verify-ca" : "verify-full"));
+
+	if (hba->pamservice)
+		options[noptions++] =
+			CStringGetTextDatum(psprintf("pamservice=%s", hba->pamservice));
+
+	if (hba->auth_method == uaLDAP)
+	{
+		if (hba->ldapserver)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapserver=%s", hba->ldapserver));
+
+		if (hba->ldapport)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapport=%d", hba->ldapport));
+
+		if (hba->ldaptls)
+			options[noptions++] =
+				CStringGetTextDatum("ldaptls=true");
+
+		if (hba->ldapprefix)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapprefix=%s", hba->ldapprefix));
+
+		if (hba->ldapsuffix)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsuffix=%s", hba->ldapsuffix));
+
+		if (hba->ldapbasedn)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapbasedn=%s", hba->ldapbasedn));
+
+		if (hba->ldapbinddn)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapbinddn=%s", hba->ldapbinddn));
+
+		if (hba->ldapbindpasswd)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapbindpasswd=%s",
+											 hba->ldapbindpasswd));
+
+		if (hba->ldapsearchattribute)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
+											 hba->ldapsearchattribute));
+
+		if (hba->ldapsearchfilter)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
+											 hba->ldapsearchfilter));
+
+		if (hba->ldapscope)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
+	}
+
+	if (hba->auth_method == uaRADIUS)
+	{
+		if (hba->radiusservers_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiusservers=%s", hba->radiusservers_s));
+
+		if (hba->radiussecrets_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiussecrets=%s", hba->radiussecrets_s));
+
+		if (hba->radiusidentifiers_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiusidentifiers=%s", hba->radiusidentifiers_s));
+
+		if (hba->radiusports_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s));
+	}
+
+	/* If you add more options, consider increasing MAX_HBA_OPTIONS. */
+	Assert(noptions <= MAX_HBA_OPTIONS);
+
+	if (noptions > 0)
+		return construct_array(options, noptions, TEXTOID, -1, false, TYPALIGN_INT);
+	else
+		return NULL;
+}
+
+/*
+ * SQL-accessible SRF to return all the entries in the pg_hba.conf file.
+ */
+Datum
+pg_hba_file_rules(PG_FUNCTION_ARGS)
+{
+	Tuplestorestate *tuple_store;
+	TupleDesc	tupdesc;
+	MemoryContext old_cxt;
+	ReturnSetInfo *rsi;
+
+	/*
+	 * We must use the Materialize mode to be safe against HBA file changes
+	 * while the cursor is open. It's also more efficient than having to look
+	 * up our current position in the parsed list every time.
+	 */
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	/* Check to see if caller supports us returning a tuplestore */
+	if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsi->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	rsi->returnMode = SFRM_Materialize;
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Build tuplestore to hold the result rows */
+	old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
+
+	tuple_store =
+		tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random,
+							  false, work_mem);
+	rsi->setDesc = tupdesc;
+	rsi->setResult = tuple_store;
+
+	MemoryContextSwitchTo(old_cxt);
+
+	/* Fill the tuplestore */
+	fill_hba_view(tuple_store, tupdesc);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8d9f3821b1..19924dca67 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -122,6 +122,16 @@ typedef struct HbaLine
 	char	   *radiusports_s;
 } HbaLine;
 
+/*
+ * A single string token lexed from a config file, together with whether
+ * the token had been quoted.
+ */
+typedef struct HbaToken
+{
+	char	   *string;
+	bool		quoted;
+} HbaToken;
+
 typedef struct IdentLine
 {
 	int			linenumber;
@@ -132,6 +142,22 @@ typedef struct IdentLine
 	regex_t		re;
 } IdentLine;
 
+/*
+ * TokenizedAuthLine represents one line lexed from a config file.
+ * Each item in the "fields" list is a sub-list of HbaTokens.
+ * We don't emit a TokenizedAuthLine for empty or all-comment lines,
+ * so "fields" is never NIL (nor are any of its sub-lists).
+ * Exception: if an error occurs during tokenization, we might
+ * have fields == NIL, in which case err_msg != NULL.
+ */
+typedef struct TokenizedAuthLine
+{
+	List	   *fields;			/* List of lists of HbaTokens */
+	int			line_num;		/* Line number */
+	char	   *raw_line;		/* Raw line text */
+	char	   *err_msg;		/* Error message if any */
+} TokenizedAuthLine;
+
 /* kluge to avoid including libpq/libpq-be.h here */
 typedef struct Port hbaPort;
 
@@ -142,6 +168,9 @@ extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
 						  bool case_sensitive);
+extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
+extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
+										List **tok_lines, int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index c6b302c7b2..f3a577652f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2694,7 +2694,7 @@ ToastTupleContext
 ToastedAttribute
 TocEntry
 TokenAuxData
-TokenizedLine
+TokenizedAuthLine
 TrackItem
 TransInvalidationInfo
 TransState
-- 
2.33.1

v2-0002-Add-a-pg_ident_file_mappings-view.patchtext/plain; charset=us-asciiDownload
From cab9d72528418d76885e3aee58cac07167602f48 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 17:38:34 +0800
Subject: [PATCH v2 2/4] Add a pg_ident_file_mappings view.

This view is similar to pg_hba_file_rules view, and can be also helpful to help
diagnosing configuration problems.

A following commit will add the possibility to include files in pg_hba and
pg_ident configuration files, which will then make this view even more useful.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml             | 108 ++++++++++++++
 doc/src/sgml/client-auth.sgml          |  10 ++
 doc/src/sgml/func.sgml                 |   5 +-
 src/backend/catalog/system_views.sql   |   6 +
 src/backend/libpq/hba.c                | 191 +++++++++++++------------
 src/backend/utils/adt/hbafuncs.c       | 164 +++++++++++++++++++++
 src/include/catalog/pg_proc.dat        |   7 +
 src/include/libpq/hba.h                |   1 +
 src/test/regress/expected/rules.out    |   6 +
 src/test/regress/expected/sysviews.out |   7 +
 src/test/regress/sql/sysviews.sql      |   3 +
 11 files changed, 412 insertions(+), 96 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 83987a9904..0de40a9626 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9530,6 +9530,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <entry>summary of client authentication configuration file contents</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link></entry>
+      <entry>summary of client user name mapping configuration file contents</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-indexes"><structname>pg_indexes</structname></link></entry>
       <entry>indexes</entry>
@@ -10523,6 +10528,109 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </para>
  </sect1>
 
+ <sect1 id="view-pg-ident-file-mappings">
+  <title><structname>pg_ident_file_mappings</structname></title>
+
+  <indexterm zone="view-pg-ident-file-mappings">
+   <primary>pg_ident_file_mappings</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_ident_file_mappings</structname> provides a summary
+   of the contents of the client user name mapping configuration file,
+   <link linkend="auth-username-maps"><filename>pg_ident.conf</filename></link>.
+   A row appears in this view for each
+   non-empty, non-comment line in the file, with annotations indicating
+   whether the rule could be applied successfully.
+  </para>
+
+  <para>
+   This view can be helpful for checking whether planned changes in the
+   authentication configuration file will work, or for diagnosing a previous
+   failure.  Note that this view reports on the <emphasis>current</emphasis>
+   contents of the file, not on what was last loaded by the server.
+  </para>
+
+  <para>
+   By default, the <structname>pg_ident_file_mappings</structname> view can be
+   read only by superusers.
+  </para>
+
+  <table>
+   <title><structname>pg_ident_file_mappings</structname> Columns</title> <tgroup
+   cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this rule in <filename>pg_ident.conf</filename>
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>map_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the map
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>sys_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Detected user name of the client
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pg_username</structfield> <type>text</type>
+      </para>
+      <para>
+       Requested PostgreSQL user name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>error</structfield> <type>text</type>
+      </para>
+      <para>
+       If not null, an error message indicating why this line could not be
+       processed
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+  <para>
+   Usually, a row reflecting an incorrect entry will have values for only
+   the <structfield>line_number</structfield> and <structfield>error</structfield> fields.
+  </para>
+
+  <para>
+   See <xref linkend="client-authentication"/> for more information about
+   client authentication configuration.
+  </para>
+ </sect1>
+
  <sect1 id="view-pg-indexes">
   <title><structname>pg_indexes</structname></title>
 
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..142b0affcb 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -896,6 +896,16 @@ mymap   /^(.*)@otherdomain\.com$   guest
    -HUP</literal>) to make it re-read the file.
   </para>
 
+  <para>
+   The system view
+   <link linkend="view-pg-ident-file-mappings"><structname>pg_ident_file_mappings</structname></link>
+   can be helpful for pre-testing changes to the
+   <filename>pg_ident.conf</filename> file, or for diagnosing problems if
+   loading of the file did not have the desired effects.  Rows in the view with
+   non-null <structfield>error</structfield> fields indicate problems in the
+   corresponding lines of the file.
+  </para>
+
   <para>
    A <filename>pg_ident.conf</filename> file that could be used in
    conjunction with the <filename>pg_hba.conf</filename> file in <xref
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index df3cd5987b..bbac492043 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25475,8 +25475,9 @@ SELECT collation for ('foo' COLLATE "de_DE");
         sending a <systemitem>SIGHUP</systemitem> signal to the postmaster
         process, which in turn sends <systemitem>SIGHUP</systemitem> to each
         of its children.) You can use the
-        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link> and
-        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> views
+        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link>,
+        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> and
+        <link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link> views
         to check the configuration files for possible errors, before reloading.
        </para></entry>
       </row>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 3cb69b1f87..ef13f470b3 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -607,6 +607,12 @@ CREATE VIEW pg_hba_file_rules AS
 REVOKE ALL ON pg_hba_file_rules FROM PUBLIC;
 REVOKE EXECUTE ON FUNCTION pg_hba_file_rules() FROM PUBLIC;
 
+CREATE VIEW pg_ident_file_mappings AS
+   SELECT * FROM pg_ident_file_mappings() AS A;
+
+REVOKE ALL ON pg_ident_file_mappings FROM PUBLIC;
+REVOKE EXECUTE ON FUNCTION pg_ident_file_mappings() FROM PUBLIC;
+
 CREATE VIEW pg_timezone_abbrevs AS
     SELECT * FROM pg_timezone_abbrevs();
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 184df4942a..e673ff5474 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -766,25 +766,22 @@ do { \
 } while (0)
 
 /*
- * Macros for handling pg_ident problems.
- * Much as above, but currently the message level is hardwired as LOG
- * and there is no provision for an err_msg string.
+ * Macros for handling pg_ident problems, similar as above.
  *
  * IDENT_FIELD_ABSENT:
- * Log a message and exit the function if the given ident field ListCell is
- * not populated.
+ * Reports when the given ident field ListCell is not populated.
  *
  * IDENT_MULTI_VALUE:
- * Log a message and exit the function if the given ident token List has more
- * than one element.
+ * Report when the given ident token List has more than one element.
  */
 #define IDENT_FIELD_ABSENT(field) \
 do { \
 	if (!field) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
 						IdentFileName, line_num))); \
+		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
 } while (0)
@@ -792,11 +789,12 @@ do { \
 #define IDENT_MULTI_VALUE(tokens) \
 do { \
 	if (tokens->length > 1) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
 							line_num, IdentFileName))); \
+		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
 } while (0)
@@ -1436,90 +1434,6 @@ load_hba(void)
 	return true;
 }
 
-/*
- * Parse one tokenised line from the ident config file and store the result in
- * an IdentLine structure.
- *
- * If parsing fails, log a message and return NULL.
- *
- * If ident_user is a regular expression (ie. begins with a slash), it is
- * compiled and stored in IdentLine structure.
- *
- * Note: this function leaks memory when an error occurs.  Caller is expected
- * to have set a memory context that will be reset if this function returns
- * NULL.
- */
-static IdentLine *
-parse_ident_line(TokenizedAuthLine *tok_line)
-{
-	int			line_num = tok_line->line_num;
-	ListCell   *field;
-	List	   *tokens;
-	HbaToken   *token;
-	IdentLine  *parsedline;
-
-	Assert(tok_line->fields != NIL);
-	field = list_head(tok_line->fields);
-
-	parsedline = palloc0(sizeof(IdentLine));
-	parsedline->linenumber = line_num;
-
-	/* Get the map token (must exist) */
-	tokens = lfirst(field);
-	IDENT_MULTI_VALUE(tokens);
-	token = linitial(tokens);
-	parsedline->usermap = pstrdup(token->string);
-
-	/* Get the ident user token */
-	field = lnext(tok_line->fields, field);
-	IDENT_FIELD_ABSENT(field);
-	tokens = lfirst(field);
-	IDENT_MULTI_VALUE(tokens);
-	token = linitial(tokens);
-	parsedline->ident_user = pstrdup(token->string);
-
-	/* Get the PG rolename token */
-	field = lnext(tok_line->fields, field);
-	IDENT_FIELD_ABSENT(field);
-	tokens = lfirst(field);
-	IDENT_MULTI_VALUE(tokens);
-	token = linitial(tokens);
-	parsedline->pg_role = pstrdup(token->string);
-
-	if (parsedline->ident_user[0] == '/')
-	{
-		/*
-		 * When system username starts with a slash, treat it as a regular
-		 * expression. Pre-compile it.
-		 */
-		int			r;
-		pg_wchar   *wstr;
-		int			wlen;
-
-		wstr = palloc((strlen(parsedline->ident_user + 1) + 1) * sizeof(pg_wchar));
-		wlen = pg_mb2wchar_with_len(parsedline->ident_user + 1,
-									wstr, strlen(parsedline->ident_user + 1));
-
-		r = pg_regcomp(&parsedline->re, wstr, wlen, REG_ADVANCED, C_COLLATION_OID);
-		if (r)
-		{
-			char		errstr[100];
-
-			pg_regerror(r, &parsedline->re, errstr, sizeof(errstr));
-			ereport(LOG,
-					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
-					 errmsg("invalid regular expression \"%s\": %s",
-							parsedline->ident_user + 1, errstr)));
-
-			pfree(wstr);
-			return NULL;
-		}
-		pfree(wstr);
-	}
-
-	return parsedline;
-}
-
 /*
  *	Process one line from the parsed ident config lines.
  *
@@ -1761,7 +1675,7 @@ load_ident(void)
 			continue;
 		}
 
-		if ((newline = parse_ident_line(tok_line)) == NULL)
+		if ((newline = parse_ident_line(tok_line, LOG)) == NULL)
 		{
 			/* Parse error; remember there's trouble */
 			ok = false;
@@ -2596,6 +2510,95 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	return parsedline;
 }
 
+/*
+ * Parse one tokenised line from the ident config file and store the result in
+ * an IdentLine structure.
+ *
+ * If parsing fails, log a message at ereport level elevel, store an error
+ * string in tok_line->err_msg and return NULL.
+ *
+ * If ident_user is a regular expression (ie. begins with a slash), it is
+ * compiled and stored in IdentLine structure.
+ *
+ * Note: this function leaks memory when an error occurs.  Caller is expected
+ * to have set a memory context that will be reset if this function returns
+ * NULL.
+ */
+IdentLine *
+parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
+{
+	int			line_num = tok_line->line_num;
+	char	  **err_msg = &tok_line->err_msg;
+	ListCell   *field;
+	List	   *tokens;
+	HbaToken   *token;
+	IdentLine  *parsedline;
+
+	Assert(tok_line->fields != NIL);
+	field = list_head(tok_line->fields);
+
+	parsedline = palloc0(sizeof(IdentLine));
+	parsedline->linenumber = line_num;
+
+	/* Get the map token (must exist) */
+	tokens = lfirst(field);
+	IDENT_MULTI_VALUE(tokens);
+	token = linitial(tokens);
+	parsedline->usermap = pstrdup(token->string);
+
+	/* Get the ident user token */
+	field = lnext(tok_line->fields, field);
+	IDENT_FIELD_ABSENT(field);
+	tokens = lfirst(field);
+	IDENT_MULTI_VALUE(tokens);
+	token = linitial(tokens);
+	parsedline->ident_user = pstrdup(token->string);
+
+	/* Get the PG rolename token */
+	field = lnext(tok_line->fields, field);
+	IDENT_FIELD_ABSENT(field);
+	tokens = lfirst(field);
+	IDENT_MULTI_VALUE(tokens);
+	token = linitial(tokens);
+	parsedline->pg_role = pstrdup(token->string);
+
+	if (parsedline->ident_user[0] == '/')
+	{
+		/*
+		 * When system username starts with a slash, treat it as a regular
+		 * expression. Pre-compile it.
+		 */
+		int			r;
+		pg_wchar   *wstr;
+		int			wlen;
+
+		wstr = palloc((strlen(parsedline->ident_user + 1) + 1) * sizeof(pg_wchar));
+		wlen = pg_mb2wchar_with_len(parsedline->ident_user + 1,
+									wstr, strlen(parsedline->ident_user + 1));
+
+		r = pg_regcomp(&parsedline->re, wstr, wlen, REG_ADVANCED, C_COLLATION_OID);
+		if (r)
+		{
+			char		errstr[100];
+
+			pg_regerror(r, &parsedline->re, errstr, sizeof(errstr));
+			ereport(elevel,
+					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+					 errmsg("invalid regular expression \"%s\": %s",
+							parsedline->ident_user + 1, errstr)));
+
+			*err_msg = psprintf("invalid regular expression \"%s\": %s",
+								parsedline->ident_user + 1, errstr);
+
+			pfree(wstr);
+			return NULL;
+		}
+		pfree(wstr);
+	}
+
+	return parsedline;
+}
+
 /*
  * Tokenize the given file.
  *
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 2bcaebe99e..2189c298b8 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -28,6 +28,9 @@
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 						  int lineno, HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int lineno, IdentLine *ident, const char *err_msg);
+static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static ArrayType *gethba_options(HbaLine *hba);
 
 
@@ -277,6 +280,116 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	MemoryContextDelete(hbacxt);
 }
 
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+
+/*
+ * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
+ * tuplestore
+ *
+ * tuple_store: where to store data
+ * tupdesc: tuple descriptor for the view
+ * lineno: pg_hba.conf line number (must always be valid)
+ * ident: parsed line data (can be NULL, in which case err_msg should be set)
+ * err_msg: error message (NULL if none)
+ *
+ * Note: leaks memory, but we don't care since this is run in a short-lived
+ * memory context.
+ */
+static void
+fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+				int lineno, IdentLine *ident, const char *err_msg)
+{
+	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	HeapTuple	tuple;
+	int			index;
+
+	Assert(tupdesc->natts == NUM_PG_IDENT_FILE_MAPPINGS_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (ident != NULL)
+	{
+		values[index++] = CStringGetTextDatum(ident->usermap);
+		values[index++] = CStringGetTextDatum(ident->ident_user);
+		values[index++] = CStringGetTextDatum(ident->pg_role);
+	}
+	else
+	{
+		/* no parsing result, so set relevant fields to nulls */
+		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * Read the pg_ident.conf file and fill the tuplestore with view records.
+ */
+static void
+fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *ident_lines = NIL;
+	ListCell   *line;
+	MemoryContext linecxt;
+	MemoryContext identcxt;
+	MemoryContext oldcxt;
+
+	/*
+	 * In the unlikely event that we can't open pg_hba.conf, we throw an
+	 * error, rather than trying to report it via some sort of view entry.
+	 * (Most other error conditions should result in a message in a view
+	 * entry.)
+	 */
+	file = AllocateFile(IdentFileName, "r");
+	if (file == NULL)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open usermap file \"%s\": %m",
+						IdentFileName)));
+
+	linecxt = tokenize_auth_file(HbaFileName, file, &ident_lines, DEBUG3);
+	FreeFile(file);
+
+	/* Now parse all the lines */
+	identcxt = AllocSetContextCreate(CurrentMemoryContext,
+									 "ident parser context",
+									 ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(identcxt);
+	foreach(line, ident_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		IdentLine   *identline = NULL;
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg == NULL)
+			identline = parse_ident_line(tok_line, DEBUG3);
+
+		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
+					  tok_line->err_msg);
+	}
+
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	/* Free parse_ident_line memory */
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(identcxt);
+}
+
 /*
  * This macro specifies the maximum number of authentication options
  * that are possible with any given authentication method that is supported.
@@ -450,3 +563,54 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 
 	PG_RETURN_NULL();
 }
+
+/*
+ * SQL-accessible SRF to return all the entries in the pg_ident.conf file.
+ */
+Datum
+pg_ident_file_mappings(PG_FUNCTION_ARGS)
+{
+	Tuplestorestate *tuple_store;
+	TupleDesc	tupdesc;
+	MemoryContext old_cxt;
+	ReturnSetInfo *rsi;
+
+	/*
+	 * We must use the Materialize mode to be safe against ident file changes
+	 * while the cursor is open. It's also more efficient than having to look
+	 * up our current position in the parsed list every time.
+	 */
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	/* Check to see if caller supports us returning a tuplestore */
+	if (rsi == NULL || !IsA(rsi, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsi->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	rsi->returnMode = SFRM_Materialize;
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	/* Build tuplestore to hold the result rows */
+	old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory);
+
+	tuple_store =
+		tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random,
+							  false, work_mem);
+	rsi->setDesc = tupdesc;
+	rsi->setResult = tuple_store;
+
+	MemoryContextSwitchTo(old_cxt);
+
+	/* Fill the tuplestore */
+	fill_ident_view(tuple_store, tupdesc);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 7de8cfc7e9..04d4edb228 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6120,6 +6120,13 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o}',
   proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
+{ oid => '9556', descr => 'show pg_ident.conf mappings',
+  proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
+  provolatile => 'v', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o}',
+  proargnames => '{line_number,map_name,sys_name,pg_usernamee,error}',
+  prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 19924dca67..fce7db248b 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -169,6 +169,7 @@ extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
 						  bool case_sensitive);
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
+extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
 										List **tok_lines, int elevel);
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 1420288d67..62cf0d8674 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1347,6 +1347,12 @@ pg_hba_file_rules| SELECT a.line_number,
     a.options,
     a.error
    FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.line_number,
+    a.map_name,
+    a.sys_name,
+    a.pg_usernamee,
+    a.error
+   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_usernamee, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 442eeb1e3f..d8a7df9498 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -55,6 +55,13 @@ select count(*) > 0 as ok from pg_hba_file_rules;
  t
 (1 row)
 
+-- We expect no user mapping in this test
+select count(*) = 0 as ok from pg_ident_file_mappings;
+ ok 
+----
+ t
+(1 row)
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
  ok 
diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql
index 4980f07be2..4c1129e787 100644
--- a/src/test/regress/sql/sysviews.sql
+++ b/src/test/regress/sql/sysviews.sql
@@ -28,6 +28,9 @@ select count(*) >= 0 as ok from pg_file_settings;
 -- There will surely be at least one rule
 select count(*) > 0 as ok from pg_hba_file_rules;
 
+-- We expect no user mapping in this test
+select count(*) = 0 as ok from pg_ident_file_mappings;
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
 
-- 
2.33.1

v2-0003-Allow-file-inclusion-in-pg_hba-and-pg_ident-files.patchtext/plain; charset=us-asciiDownload
From b2000c98156dc85de08db32b9b71a2f6a4528f1d Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 15:45:26 +0800
Subject: [PATCH v2 3/4] Allow file inclusion in pg_hba and pg_ident files.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml             |  48 +++-
 doc/src/sgml/client-auth.sgml          |  34 ++-
 src/backend/libpq/hba.c                | 373 ++++++++++++++++---------
 src/backend/libpq/pg_hba.conf.sample   |   8 +-
 src/backend/libpq/pg_ident.conf.sample |   8 +-
 src/backend/utils/adt/hbafuncs.c       |  53 +++-
 src/include/catalog/pg_proc.dat        |  12 +-
 src/include/libpq/hba.h                |   2 +
 src/test/regress/expected/rules.out    |  12 +-
 9 files changed, 395 insertions(+), 155 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 0de40a9626..07c6679a52 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -10430,12 +10430,31 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule in the given file_name
       </para></entry>
      </row>
 
@@ -10571,6 +10590,33 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this mapping if the mapping is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this mapping
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this mapping in the given file_name
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 142b0affcb..e1d0e103b3 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,17 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,6 +112,7 @@
   <para>
    A record can have several formats:
 <synopsis>
+include       <replaceable>file</replaceable>
 local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
 host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
@@ -118,6 +128,15 @@ hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceabl
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -835,8 +854,9 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -847,6 +867,14 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   The lines can record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index e673ff5474..85988fdeda 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -68,6 +68,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -112,6 +118,12 @@ static const char *const UserAuthName[] =
 };
 
 
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static void tokenize_auth_file_with_context(MemoryContext Linecxt,
+											const char *filename, FILE *file,
+											List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
@@ -327,6 +339,68 @@ next_field_expand(const char *filename, char **lineptr,
 	return tokens;
 }
 
+/*
+ * Open the given file for inclusion in an authentication file, whether
+ * secondary or included.
+*/
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+		const char *outer_filename, int elevel, char **err_msg,
+		char **inc_fullname)
+{
+	FILE       *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+				strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int         save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		switch (kind)
+		{
+			case SecondaryAuthFile:
+				msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+				msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+				break;
+			case IncludedAuthFile:
+				msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+				msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+				break;
+			default:
+				elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+				break;
+		}
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg(msglog, inc_filename, *inc_fullname)));
+		*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+				strerror(save_errno));
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
 /*
  * tokenize_inc_file
  *		Expand a file included from another file into an hba "field"
@@ -355,36 +429,11 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, outer_filename,
+							 elevel, err_msg, &inc_fullname);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
 	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
@@ -423,6 +472,169 @@ tokenize_inc_file(List *tokens,
 	return tokens;
 }
 
+/*
+ * Tokenize the given file.
+ *
+ * The output is a list of TokenizedAuthLine structs; see struct definition
+ * above.
+ *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
+ * filename: the absolute path to the target file
+ * file: the already-opened target file
+ * tok_lines: receives output list
+ * elevel: message logging level
+ *
+ * Errors are reported by logging messages at ereport level elevel and by
+ * adding TokenizedAuthLine structs containing non-null err_msg fields to the
+ * output list.
+ *
+ */
+static void
+tokenize_auth_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel)
+{
+	StringInfoData buf;
+	int			line_number = 1;
+	MemoryContext oldcxt;
+
+	oldcxt = MemoryContextSwitchTo(linecxt);
+
+	initStringInfo(&buf);
+
+	while (!feof(file) && !ferror(file))
+	{
+		TokenizedAuthLine *tok_line;
+		char	   *lineptr;
+		List	   *current_line = NIL;
+		char	   *err_msg = NULL;
+		int			last_backslash_buflen = 0;
+		int			continuations = 0;
+
+		/* Collect the next input line, handling backslash continuations */
+		resetStringInfo(&buf);
+
+		while (pg_get_line_append(file, &buf, NULL))
+		{
+			/* Strip trailing newline, including \r in case we're on Windows */
+			buf.len = pg_strip_crlf(buf.data);
+
+			/*
+			 * Check for backslash continuation.  The backslash must be after
+			 * the last place we found a continuation, else two backslashes
+			 * followed by two \n's would behave surprisingly.
+			 */
+			if (buf.len > last_backslash_buflen &&
+				buf.data[buf.len - 1] == '\\')
+			{
+				/* Continuation, so strip it and keep reading */
+				buf.data[--buf.len] = '\0';
+				last_backslash_buflen = buf.len;
+				continuations++;
+				continue;
+			}
+
+			/* Nope, so we have the whole line */
+			break;
+		}
+
+		if (ferror(file))
+		{
+			/* I/O error! */
+			int			save_errno = errno;
+
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg("could not read file \"%s\": %m", filename)));
+			err_msg = psprintf("could not read file \"%s\": %s",
+							   filename, strerror(save_errno));
+			break;
+		}
+
+		/* Parse fields */
+		lineptr = buf.data;
+		while (*lineptr && err_msg == NULL)
+		{
+			List	   *current_field;
+
+			current_field = next_field_expand(filename, &lineptr,
+											  elevel, &err_msg);
+			/* add field to line, unless we are at EOL or comment start */
+			if (current_field != NIL)
+				current_line = lappend(current_line, current_field);
+		}
+
+		/*
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
+		 */
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			HbaToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+				char	   *inc_fullname;
+				FILE	   *inc_file;
+
+				inc_filename = second->string;
+
+				inc_file = open_inc_file(IncludedAuthFile, inc_filename,
+										 filename, elevel, &err_msg,
+										 &inc_fullname);
+
+				/*
+				 * The included file could be open, now recursively process it.
+				 * Errors will be reported in the general TokenizedAuthLine
+				 * processing.
+				 */
+				if (inc_file != NULL)
+				{
+					tokenize_auth_file_with_context(linecxt, inc_fullname,
+													inc_file, tok_lines,
+													elevel);
+
+					FreeFile(inc_file);
+					pfree(inc_fullname);
+
+					/*
+					 * The line is fulle processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+				else
+				{
+					/* We should got an error */
+					Assert(err_msg != NULL);
+				}
+			}
+		}
+
+		/* General processing: Emit line to the TokenizedAuthLine */
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
+		line_number += continuations + 1;
+
+	}
+
+	MemoryContextSwitchTo(oldcxt);
+}
 
 /*
  * Does user belong to role?
@@ -1796,6 +2008,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(tok_line->file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -2602,118 +2815,26 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 /*
  * Tokenize the given file.
  *
- * The output is a list of TokenizedAuthLine structs; see struct definition
- * above.
+ * Wrapper around tokenize_auth_file_with_context, creating a dedicated memory
+ * context.
  *
- * filename: the absolute path to the target file
- * file: the already-opened target file
- * tok_lines: receives output list
- * elevel: message logging level
- *
- * Errors are reported by logging messages at ereport level elevel and by
- * adding TokenizedAuthLine structs containing non-null err_msg fields to the
- * output list.
- *
- * Return value is a memory context which contains all memory allocated by
+ * Return value is this memory context which contains all memory allocated by
  * this function (it's a child of caller's context).
  */
 MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int elevel)
 {
-	int			line_number = 1;
-	StringInfoData buf;
 	MemoryContext linecxt;
-	MemoryContext oldcxt;
 
 	linecxt = AllocSetContextCreate(CurrentMemoryContext,
 									"tokenize_auth_file",
 									ALLOCSET_SMALL_SIZES);
-	oldcxt = MemoryContextSwitchTo(linecxt);
-
-	initStringInfo(&buf);
 
 	*tok_lines = NIL;
 
-	while (!feof(file) && !ferror(file))
-	{
-		char	   *lineptr;
-		List	   *current_line = NIL;
-		char	   *err_msg = NULL;
-		int			last_backslash_buflen = 0;
-		int			continuations = 0;
-
-		/* Collect the next input line, handling backslash continuations */
-		resetStringInfo(&buf);
-
-		while (pg_get_line_append(file, &buf, NULL))
-		{
-			/* Strip trailing newline, including \r in case we're on Windows */
-			buf.len = pg_strip_crlf(buf.data);
-
-			/*
-			 * Check for backslash continuation.  The backslash must be after
-			 * the last place we found a continuation, else two backslashes
-			 * followed by two \n's would behave surprisingly.
-			 */
-			if (buf.len > last_backslash_buflen &&
-				buf.data[buf.len - 1] == '\\')
-			{
-				/* Continuation, so strip it and keep reading */
-				buf.data[--buf.len] = '\0';
-				last_backslash_buflen = buf.len;
-				continuations++;
-				continue;
-			}
-
-			/* Nope, so we have the whole line */
-			break;
-		}
-
-		if (ferror(file))
-		{
-			/* I/O error! */
-			int			save_errno = errno;
-
-			ereport(elevel,
-					(errcode_for_file_access(),
-					 errmsg("could not read file \"%s\": %m", filename)));
-			err_msg = psprintf("could not read file \"%s\": %s",
-							   filename, strerror(save_errno));
-			break;
-		}
-
-		/* Parse fields */
-		lineptr = buf.data;
-		while (*lineptr && err_msg == NULL)
-		{
-			List	   *current_field;
-
-			current_field = next_field_expand(filename, &lineptr,
-											  elevel, &err_msg);
-			/* add field to line, unless we are at EOL or comment start */
-			if (current_field != NIL)
-				current_line = lappend(current_line, current_field);
-		}
-
-		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
-		 * */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedAuthLine *tok_line;
-
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
-		}
-
-		line_number += continuations + 1;
-	}
-
-	MemoryContextSwitchTo(oldcxt);
+	tokenize_auth_file_with_context(linecxt, filename, file, tok_lines,
+									elevel);
 
 	return linecxt;
 }
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..0b6589a7b9 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,6 +9,7 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
+# include       FILE
 # local         DATABASE  USER  METHOD  [OPTIONS]
 # host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 # hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
@@ -18,7 +19,12 @@
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", it's not a mapping record but a directive to
+# include records from another file, specified in the field.  FILE is the file
+# to include.  It can be specified with a relative or absolute path, and can be
+# double quoted if it contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..138359cf03 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,18 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
+# include  FILE
 # MAPNAME  SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", it's not an authentication record but a
+# directive to include records from another file, specified in the field.  FILE
+# is the file to include.  It can be specified with a relative or absolute
+# path, and can be double quoted if it contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 2189c298b8..4c9df29427 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,16 +26,18 @@
 
 
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int tule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int mappint_number, const char *filename,
 							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static ArrayType *gethba_options(HbaLine *hba);
 
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line: build one row of pg_hba_file_rules view, add it to tuplestore
@@ -51,7 +53,8 @@ static ArrayType *gethba_options(HbaLine *hba);
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -70,6 +73,13 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -213,7 +223,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -235,6 +245,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -265,12 +276,15 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
 		HbaLine    *hbaline = NULL;
 
-		/* don't parse lines that already have errors */
+		/* Only parse lines that don't already have an error */
 		if (tok_line->err_msg == NULL)
+		{
 			hbaline = parse_hba_line(tok_line, DEBUG3);
+			rule_number++;
+		}
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -281,7 +295,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -298,7 +312,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -311,6 +326,13 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -323,7 +345,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -345,6 +367,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -375,12 +398,16 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
 		IdentLine   *identline = NULL;
 
-		/* don't parse lines that already have errors */
+		/* Only parse lines that don't already have an error */
 		if (tok_line->err_msg == NULL)
+		{
 			identline = parse_ident_line(tok_line, DEBUG3);
+			mapping_number++;
+		}
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-					  tok_line->err_msg);
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 04d4edb228..e3baafc3e3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6116,16 +6116,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '9556', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_usernamee,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_usernamee,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index fce7db248b..551e961585 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -153,6 +154,7 @@ typedef struct IdentLine
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of HbaTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 62cf0d8674..e6f274cb59 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,9 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1348,15 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_usernamee,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_usernamee, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_usernamee, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.33.1

v2-0004-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From 99064a11f3ef7744d06d415322945da31c2cd28d Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v2 4/4] POC: Add a pg_hba_matches() function.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index fd1421788e..ae839a4b76 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 85988fdeda..e3e4a0ce7d 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -26,6 +26,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -41,6 +42,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2838,3 +2840,139 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 
 	return linecxt;
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e3baafc3e3..a8577a9948 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6127,6 +6127,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_usernamee,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.33.1

#10Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#9)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Feb 28, 2022 at 07:42:17PM +0800, Julien Rouhaud wrote:

Done in attached v2. I did the split in a separate commit, as the diff is
otherwise unreadable. While at it I also fixed a few minor issues (I missed a
MemoryContextDelete, and now avoid relying on inet_net_pton which apparently
doesn't exist in cygwin).

Hmm. The diffs of 0001 are really hard to read. Do you know why this
is happening? Is that because some code has been moved around? I
have been doing a comparison of all the routines showing up in the
diffs, to note that the contents of load_hba(), load_ident(),
hba_getauthmethod() & friends are actually the same, but this makes
the change history harder to follow. Moving around fill_hba_line()
and fill_hba_view() should be enough, indeed.

+#include "utils/guc.h"
+//#include "utils/tuplestore.h"

Ditto.

+   /* Build a tuple descriptor for our result type */
+   if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+       elog(ERROR, "return type must be a row type");

Worth noting that I was planning to apply a patch from Melanie
Plageman to simplify the creation of tupledesc and tuplestores for
set-returning functions like this one, so this would cut a bit of code
here. This is not directly related to your patch, though, that's my
business :)

Well, as of 0002, one thing that makes things harder to follow is that
parse_ident_line() is moved at a different place in hba.c, one slight
difference being err_msg to store the error message in the token
line.. But shouldn't the extension of parse_ident_line() with its
elevel be included in 0001? Or, well, it could just be done in its
own patch to make for a cleaner history, so as 0002 could be shaped as
two commits itself.

Also, it seems to me that we'd better have some TAP tests for that to
make sure of its contents? One place would be src/test/auth/.
Another place where we make use of user mapping is the krb5 tests but
these are run in a limited fashion in the buildfarm. We also set some
mappings for SSPI on Windows all the time, so we'd better be careful
about that and paint some $windows_os in the tests when looking at the
output of the view.

+-- We expect no user mapping in this test
+select count(*) = 0 as ok from pg_ident_file_mappings;

It could be possible to do installcheck on an instance that has user
mappings meaning that this had better be ">= 0", no? Does this pass
on Windows where pg_regress sets some mappings for SSPI when creating
one or more roles?
--
Michael

#11Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#10)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Tue, Mar 01, 2022 at 04:45:48PM +0900, Michael Paquier wrote:

Hmm. The diffs of 0001 are really hard to read. Do you know why this
is happening? Is that because some code has been moved around?

Yes, I followed the file convention to put the static functions first and then
the exposed functions, and git-diff makes a terrible mess out of it :(

I
have been doing a comparison of all the routines showing up in the
diffs, to note that the contents of load_hba(), load_ident(),
hba_getauthmethod() & friends are actually the same, but this makes
the change history harder to follow. Moving around fill_hba_line()
and fill_hba_view() should be enough, indeed.

There's no functional change apart from exposing some functions and moving some
in another file, so I though it's still ok to keep some consistency. There
isn't much changes backpatched in that file, so it shouldn't create more
maintenance burden than simply splitting the file.

If you prefer to interleave static and non static function I can change it.

+#include "utils/guc.h"
+//#include "utils/tuplestore.h"

Yes I noticed this one this morning. I didn't want to send a new patch version
just for that, but I already fixed it locally.

+   /* Build a tuple descriptor for our result type */
+   if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+       elog(ERROR, "return type must be a row type");

Worth noting that I was planning to apply a patch from Melanie
Plageman to simplify the creation of tupledesc and tuplestores for
set-returning functions like this one, so this would cut a bit of code
here. This is not directly related to your patch, though, that's my
business :)

Yes I'm aware of that thread. I will be happy to change the patch to use
MakeFuncResultTuplestore() as soon as it lands. Thanks for the notice though.

Well, as of 0002, one thing that makes things harder to follow is that
parse_ident_line() is moved at a different place in hba.c, one slight
difference being err_msg to store the error message in the token
line.. But shouldn't the extension of parse_ident_line() with its
elevel be included in 0001?

No, because 0001 is unrelated. The changes you mentioned (exposing the
function and adding the error reporting) are only needed for the new view
introduced in 0002, thus not part of 0001.

Or, well, it could just be done in its own patch to make for a cleaner
history, so as 0002 could be shaped as two commits itself.

It seems strange to me to add a commit (or include in a commit) just to make a
single function exposed while nothing needs it, but I can do it this way if you
prefer.

Also, it seems to me that we'd better have some TAP tests for that to
make sure of its contents?

As I mentioned in my initial email, I intentionally didn't add any test in the
patchset yet, except the exact same coverage for the new view as there's for
pg_hba_file_rules. Ideally I'd like to add tests only once, to cover both 002
and 0003. But I don't want to waste time for that right now, especially since
no one seems to be interested in 0003.

+-- We expect no user mapping in this test
+select count(*) = 0 as ok from pg_ident_file_mappings;

It could be possible to do installcheck on an instance that has user
mappings meaning that this had better be ">= 0", no?

I thought about it, and supposed it would bring a bit more value with the test
like that. I can change it if you prefer.

Does this pass
on Windows where pg_regress sets some mappings for SSPI when creating
one or more roles?

According to CI and cfbot yes. E.g.
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/37/3558.
Note that the failed runs are the warning I mentioned for mingw32 and the POC
0004, which is now fixed.

#12Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#11)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Mar 01, 2022 at 05:19:50PM +0800, Julien Rouhaud wrote:

On Tue, Mar 01, 2022 at 04:45:48PM +0900, Michael Paquier wrote:

Hmm. The diffs of 0001 are really hard to read. Do you know why this
is happening? Is that because some code has been moved around?

Yes, I followed the file convention to put the static functions first and then
the exposed functions, and git-diff makes a terrible mess out of it :(

I'd like to think that not doing such a thing would be more helpful in
this case. As the diffs show, anyone is going to have a hard time to
figure out if there are any differences in any of those routines, and
if these are the origin of a different problem. A second thing is
that this is going to make back-patching unnecessarily harder.

There's no functional change apart from exposing some functions and moving some
in another file, so I though it's still ok to keep some consistency. There
isn't much changes backpatched in that file, so it shouldn't create more
maintenance burden than simply splitting the file.

A lot of files do that already. History clarity matters most IMO.

As I mentioned in my initial email, I intentionally didn't add any test in the
patchset yet, except the exact same coverage for the new view as there's for
pg_hba_file_rules. Ideally I'd like to add tests only once, to cover both 002
and 0003. But I don't want to waste time for that right now, especially since
no one seems to be interested in 0003.

But that would be helpful for 0002. I think that we should have a bit
more coverage in this area. pg_hba_file_rules could gain in coverage,
additionally, but this is unrelated to what you are proposing here..

Does this pass
on Windows where pg_regress sets some mappings for SSPI when creating
one or more roles?

According to CI and cfbot yes. E.g.
https://cirrus-ci.com/github/postgresql-cfbot/postgresql/commitfest/37/3558.
Note that the failed runs are the warning I mentioned for mingw32 and the POC
0004, which is now fixed.

Interesting, I would not have expected that. I may poke at that more
seriously.
--
Michael

#13Julien Rouhaud
rjuju123@gmail.com
In reply to: Julien Rouhaud (#11)
4 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

The cfbot says that the patch doesn't apply anymore, so here's a v3 with the
changes mentioned below.

On Tue, Mar 01, 2022 at 05:19:50PM +0800, Julien Rouhaud wrote:

If you prefer to interleave static and non static function I can change it.

Change the split to not reorder functions.

+#include "utils/guc.h"
+//#include "utils/tuplestore.h"

Yes I noticed this one this morning. I didn't want to send a new patch version
just for that, but I already fixed it locally.

Included.

Yes I'm aware of that thread. I will be happy to change the patch to use
MakeFuncResultTuplestore() as soon as it lands. Thanks for the notice though.

Done, with the new SetSingleFuncCall().

It could be possible to do installcheck on an instance that has user
mappings meaning that this had better be ">= 0", no?

I thought about it, and supposed it would bring a bit more value with the test
like that. I can change it if you prefer.

Change this way.

Attachments:

v3-0003-Allow-file-inclusion-in-pg_hba-and-pg_ident-files.patchtext/plain; charset=us-asciiDownload
From d0fdc3c1443802087fd58259729ee83128751056 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 15:45:26 +0800
Subject: [PATCH v3 3/4] Allow file inclusion in pg_hba and pg_ident files.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml             |  48 +++++-
 doc/src/sgml/client-auth.sgml          |  34 +++-
 src/backend/libpq/hba.c                | 218 +++++++++++++++++++------
 src/backend/libpq/pg_hba.conf.sample   |   8 +-
 src/backend/libpq/pg_ident.conf.sample |   8 +-
 src/backend/utils/adt/hbafuncs.c       |  51 ++++--
 src/include/catalog/pg_proc.dat        |  12 +-
 src/include/libpq/hba.h                |   2 +
 src/test/regress/expected/rules.out    |  12 +-
 9 files changed, 314 insertions(+), 79 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index e87f0543d6..c6c5e7d08e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -10440,12 +10440,31 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule in the given file_name
       </para></entry>
      </row>
 
@@ -10581,6 +10600,33 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this mapping if the mapping is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this mapping
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this mapping in the given file_name
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 142b0affcb..e1d0e103b3 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,17 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,6 +112,7 @@
   <para>
    A record can have several formats:
 <synopsis>
+include       <replaceable>file</replaceable>
 local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
 host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
@@ -118,6 +128,15 @@ hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceabl
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -835,8 +854,9 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -847,6 +867,14 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   The lines can record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 68136f6244..1bfcaef025 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -68,6 +68,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -112,10 +118,16 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
 
 
 /*
@@ -355,36 +367,11 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, outer_filename,
+							 elevel, err_msg, &inc_fullname);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
 	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
@@ -423,12 +410,38 @@ tokenize_inc_file(List *tokens,
 	return tokens;
 }
 
+/*
+ * Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a decicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, elevel);
+
+	return linecxt;
+}
+
 /*
  * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see struct definition
  * above.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -437,30 +450,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -521,29 +526,76 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			HbaToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+				char	   *inc_fullname;
+				FILE	   *inc_file;
+
+				inc_filename = second->string;
+
+				inc_file = open_inc_file(IncludedAuthFile, inc_filename,
+										 filename, elevel, &err_msg,
+										 &inc_fullname);
+
+				/*
+				 * The included file could be open, now recursively process it.
+				 * Errors will be reported in the general TokenizedAuthLine
+				 * processing.
+				 */
+				if (inc_file != NULL)
+				{
+					tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+											   tok_lines, elevel);
+
+					FreeFile(inc_file);
+					pfree(inc_fullname);
+
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+				else
+				{
+					/* We should got an error */
+					Assert(err_msg != NULL);
+				}
+			}
 		}
 
+		/* General processing: emit line to the TokenizedAuthLine */
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
+
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -950,6 +1002,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(tok_line->file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -2298,6 +2351,67 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		switch (kind)
+		{
+			case SecondaryAuthFile:
+				msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+				msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+				break;
+			case IncludedAuthFile:
+				msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+				msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+				break;
+			default:
+				elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+				break;
+		}
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg(msglog, inc_filename, *inc_fullname)));
+		*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+							strerror(save_errno));
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..0b6589a7b9 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,6 +9,7 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
+# include       FILE
 # local         DATABASE  USER  METHOD  [OPTIONS]
 # host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 # hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
@@ -18,7 +19,12 @@
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", it's not a mapping record but a directive to
+# include records from another file, specified in the field.  FILE is the file
+# to include.  It can be specified with a relative or absolute path, and can be
+# double quoted if it contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..138359cf03 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,18 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
+# include  FILE
 # MAPNAME  SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", it's not an authentication record but a
+# directive to include records from another file, specified in the field.  FILE
+# is the file to include.  It can be specified with a relative or absolute
+# path, and can be double quoted if it contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 75e69383c2..ee12dc1893 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,9 +26,11 @@
 
 static ArrayType *gethba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int mapping_number, const char *filename,
 							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
@@ -157,7 +159,7 @@ gethba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line: build one row of pg_hba_file_rules view, add it to tuplestore
@@ -173,7 +175,8 @@ gethba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -192,6 +195,13 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -335,7 +345,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -357,6 +367,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -391,8 +402,12 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -426,7 +441,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -443,7 +458,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -456,6 +472,13 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -468,7 +491,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -490,6 +513,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -524,8 +548,13 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-					  tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 6ccbc9af4c..0e8a589302 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6111,16 +6111,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '9556', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_usernamee,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_usernamee,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index fce7db248b..551e961585 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -153,6 +154,7 @@ typedef struct IdentLine
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of HbaTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 76a209b717..5b7a4c01ee 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,9 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1348,15 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_usernamee,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_usernamee, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_usernamee, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.33.1

v3-0004-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From 711359a1a80a7bfcf350b9ffa0a2f869f15a7e81 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v3 4/4] POC: Add a pg_hba_matches() function.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 81bac6f581..049cdabc81 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 1bfcaef025..8afbb16b76 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -26,6 +26,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -41,6 +42,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2833,3 +2835,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0e8a589302..ee77bd1136 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6122,6 +6122,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_usernamee,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.33.1

v3-0001-Extract-view-processing-code-from-hba.c.patchtext/plain; charset=us-asciiDownload
From 865b181ea989bb5c52fad2c3c9e20a38aa173cf3 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 1 Mar 2022 21:45:42 +0800
Subject: [PATCH v3 1/4] Extract view processing code from hba.c

This file is already quite big and a following commit will add yet an
additional view, so let's move all the view related code in hba.c into a new
adt/hbafuncs.c.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/libpq/hba.c          | 462 ++-----------------------------
 src/backend/utils/adt/Makefile   |   1 +
 src/backend/utils/adt/hbafuncs.c | 423 ++++++++++++++++++++++++++++
 src/include/libpq/hba.h          |  29 ++
 src/tools/pgindent/typedefs.list |   2 +-
 5 files changed, 475 insertions(+), 442 deletions(-)
 create mode 100644 src/backend/utils/adt/hbafuncs.c

diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 90953c38f3..f9843a0b30 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -68,32 +68,6 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
-/*
- * A single string token lexed from a config file, together with whether
- * the token had been quoted.
- */
-typedef struct HbaToken
-{
-	char	   *string;
-	bool		quoted;
-} HbaToken;
-
-/*
- * TokenizedLine represents one line lexed from a config file.
- * Each item in the "fields" list is a sub-list of HbaTokens.
- * We don't emit a TokenizedLine for empty or all-comment lines,
- * so "fields" is never NIL (nor are any of its sub-lists).
- * Exception: if an error occurs during tokenization, we might
- * have fields == NIL, in which case err_msg != NULL.
- */
-typedef struct TokenizedLine
-{
-	List	   *fields;			/* List of lists of HbaTokens */
-	int			line_num;		/* Line number */
-	char	   *raw_line;		/* Raw line text */
-	char	   *err_msg;		/* Error message if any */
-} TokenizedLine;
-
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -138,16 +112,10 @@ static const char *const UserAuthName[] =
 };
 
 
-static MemoryContext tokenize_file(const char *filename, FILE *file,
-								   List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
-static ArrayType *gethba_options(HbaLine *hba);
-static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
-static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
 /*
@@ -419,7 +387,7 @@ tokenize_inc_file(List *tokens,
 	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -427,7 +395,7 @@ tokenize_inc_file(List *tokens,
 	/* Copy all tokens found in the file and append to the tokens list */
 	foreach(inc_line, inc_lines)
 	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(inc_line);
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(inc_line);
 		ListCell   *inc_field;
 
 		/* If any line has an error, propagate that up to caller */
@@ -458,7 +426,8 @@ tokenize_inc_file(List *tokens,
 /*
  * Tokenize the given file.
  *
- * The output is a list of TokenizedLine structs; see struct definition above.
+ * The output is a list of TokenizedAuthLine structs; see struct definition
+ * above.
  *
  * filename: the absolute path to the target file
  * file: the already-opened target file
@@ -466,14 +435,15 @@ tokenize_inc_file(List *tokens,
  * elevel: message logging level
  *
  * Errors are reported by logging messages at ereport level elevel and by
- * adding TokenizedLine structs containing non-null err_msg fields to the
+ * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
  *
  * Return value is a memory context which contains all memory allocated by
  * this function (it's a child of caller's context).
  */
-static MemoryContext
-tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int elevel)
 {
 	int			line_number = 1;
 	StringInfoData buf;
@@ -481,7 +451,7 @@ tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
 	MemoryContext oldcxt;
 
 	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_file",
+									"tokenize_auth_file",
 									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
@@ -550,12 +520,14 @@ tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
 				current_line = lappend(current_line, current_field);
 		}
 
-		/* Reached EOL; emit line to TokenizedLine list unless it's boring */
+		/*
+		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 */
 		if (current_line != NIL || err_msg != NULL)
 		{
-			TokenizedLine *tok_line;
+			TokenizedAuthLine *tok_line;
 
-			tok_line = (TokenizedLine *) palloc(sizeof(TokenizedLine));
+			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
 			tok_line->fields = current_line;
 			tok_line->line_num = line_number;
 			tok_line->raw_line = pstrdup(buf.data);
@@ -962,8 +934,8 @@ do { \
  * to have set a memory context that will be reset if this function returns
  * NULL.
  */
-static HbaLine *
-parse_hba_line(TokenizedLine *tok_line, int elevel)
+HbaLine *
+parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
 	char	  **err_msg = &tok_line->err_msg;
@@ -2257,7 +2229,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2268,7 +2240,7 @@ load_hba(void)
 	oldcxt = MemoryContextSwitchTo(hbacxt);
 	foreach(line, hba_lines)
 	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line);
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
 		HbaLine    *newline;
 
 		/* don't parse lines that already have errors */
@@ -2328,398 +2300,6 @@ load_hba(void)
 	return true;
 }
 
-/*
- * This macro specifies the maximum number of authentication options
- * that are possible with any given authentication method that is supported.
- * Currently LDAP supports 11, and there are 3 that are not dependent on
- * the auth method here.  It may not actually be possible to set all of them
- * at the same time, but we'll set the macro value high enough to be
- * conservative and avoid warnings from static analysis tools.
- */
-#define MAX_HBA_OPTIONS 14
-
-/*
- * Create a text array listing the options specified in the HBA line.
- * Return NULL if no options are specified.
- */
-static ArrayType *
-gethba_options(HbaLine *hba)
-{
-	int			noptions;
-	Datum		options[MAX_HBA_OPTIONS];
-
-	noptions = 0;
-
-	if (hba->auth_method == uaGSS || hba->auth_method == uaSSPI)
-	{
-		if (hba->include_realm)
-			options[noptions++] =
-				CStringGetTextDatum("include_realm=true");
-
-		if (hba->krb_realm)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("krb_realm=%s", hba->krb_realm));
-	}
-
-	if (hba->usermap)
-		options[noptions++] =
-			CStringGetTextDatum(psprintf("map=%s", hba->usermap));
-
-	if (hba->clientcert != clientCertOff)
-		options[noptions++] =
-			CStringGetTextDatum(psprintf("clientcert=%s", (hba->clientcert == clientCertCA) ? "verify-ca" : "verify-full"));
-
-	if (hba->pamservice)
-		options[noptions++] =
-			CStringGetTextDatum(psprintf("pamservice=%s", hba->pamservice));
-
-	if (hba->auth_method == uaLDAP)
-	{
-		if (hba->ldapserver)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapserver=%s", hba->ldapserver));
-
-		if (hba->ldapport)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapport=%d", hba->ldapport));
-
-		if (hba->ldaptls)
-			options[noptions++] =
-				CStringGetTextDatum("ldaptls=true");
-
-		if (hba->ldapprefix)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapprefix=%s", hba->ldapprefix));
-
-		if (hba->ldapsuffix)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapsuffix=%s", hba->ldapsuffix));
-
-		if (hba->ldapbasedn)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapbasedn=%s", hba->ldapbasedn));
-
-		if (hba->ldapbinddn)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapbinddn=%s", hba->ldapbinddn));
-
-		if (hba->ldapbindpasswd)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapbindpasswd=%s",
-											 hba->ldapbindpasswd));
-
-		if (hba->ldapsearchattribute)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
-											 hba->ldapsearchattribute));
-
-		if (hba->ldapsearchfilter)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
-											 hba->ldapsearchfilter));
-
-		if (hba->ldapscope)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
-	}
-
-	if (hba->auth_method == uaRADIUS)
-	{
-		if (hba->radiusservers_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiusservers=%s", hba->radiusservers_s));
-
-		if (hba->radiussecrets_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiussecrets=%s", hba->radiussecrets_s));
-
-		if (hba->radiusidentifiers_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiusidentifiers=%s", hba->radiusidentifiers_s));
-
-		if (hba->radiusports_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s));
-	}
-
-	/* If you add more options, consider increasing MAX_HBA_OPTIONS. */
-	Assert(noptions <= MAX_HBA_OPTIONS);
-
-	if (noptions > 0)
-		return construct_array(options, noptions, TEXTOID, -1, false, TYPALIGN_INT);
-	else
-		return NULL;
-}
-
-/* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
-
-/*
- * fill_hba_line: build one row of pg_hba_file_rules view, add it to tuplestore
- *
- * tuple_store: where to store data
- * tupdesc: tuple descriptor for the view
- * lineno: pg_hba.conf line number (must always be valid)
- * hba: parsed line data (can be NULL, in which case err_msg should be set)
- * err_msg: error message (NULL if none)
- *
- * Note: leaks memory, but we don't care since this is run in a short-lived
- * memory context.
- */
-static void
-fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
-{
-	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
-	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
-	char		buffer[NI_MAXHOST];
-	HeapTuple	tuple;
-	int			index;
-	ListCell   *lc;
-	const char *typestr;
-	const char *addrstr;
-	const char *maskstr;
-	ArrayType  *options;
-
-	Assert(tupdesc->natts == NUM_PG_HBA_FILE_RULES_ATTS);
-
-	memset(values, 0, sizeof(values));
-	memset(nulls, 0, sizeof(nulls));
-	index = 0;
-
-	/* line_number */
-	values[index++] = Int32GetDatum(lineno);
-
-	if (hba != NULL)
-	{
-		/* type */
-		/* Avoid a default: case so compiler will warn about missing cases */
-		typestr = NULL;
-		switch (hba->conntype)
-		{
-			case ctLocal:
-				typestr = "local";
-				break;
-			case ctHost:
-				typestr = "host";
-				break;
-			case ctHostSSL:
-				typestr = "hostssl";
-				break;
-			case ctHostNoSSL:
-				typestr = "hostnossl";
-				break;
-			case ctHostGSS:
-				typestr = "hostgssenc";
-				break;
-			case ctHostNoGSS:
-				typestr = "hostnogssenc";
-				break;
-		}
-		if (typestr)
-			values[index++] = CStringGetTextDatum(typestr);
-		else
-			nulls[index++] = true;
-
-		/* database */
-		if (hba->databases)
-		{
-			/*
-			 * Flatten HbaToken list to string list.  It might seem that we
-			 * should re-quote any quoted tokens, but that has been rejected
-			 * on the grounds that it makes it harder to compare the array
-			 * elements to other system catalogs.  That makes entries like
-			 * "all" or "samerole" formally ambiguous ... but users who name
-			 * databases/roles that way are inflicting their own pain.
-			 */
-			List	   *names = NIL;
-
-			foreach(lc, hba->databases)
-			{
-				HbaToken   *tok = lfirst(lc);
-
-				names = lappend(names, tok->string);
-			}
-			values[index++] = PointerGetDatum(strlist_to_textarray(names));
-		}
-		else
-			nulls[index++] = true;
-
-		/* user */
-		if (hba->roles)
-		{
-			/* Flatten HbaToken list to string list; see comment above */
-			List	   *roles = NIL;
-
-			foreach(lc, hba->roles)
-			{
-				HbaToken   *tok = lfirst(lc);
-
-				roles = lappend(roles, tok->string);
-			}
-			values[index++] = PointerGetDatum(strlist_to_textarray(roles));
-		}
-		else
-			nulls[index++] = true;
-
-		/* address and netmask */
-		/* Avoid a default: case so compiler will warn about missing cases */
-		addrstr = maskstr = NULL;
-		switch (hba->ip_cmp_method)
-		{
-			case ipCmpMask:
-				if (hba->hostname)
-				{
-					addrstr = hba->hostname;
-				}
-				else
-				{
-					/*
-					 * Note: if pg_getnameinfo_all fails, it'll set buffer to
-					 * "???", which we want to return.
-					 */
-					if (hba->addrlen > 0)
-					{
-						if (pg_getnameinfo_all(&hba->addr, hba->addrlen,
-											   buffer, sizeof(buffer),
-											   NULL, 0,
-											   NI_NUMERICHOST) == 0)
-							clean_ipv6_addr(hba->addr.ss_family, buffer);
-						addrstr = pstrdup(buffer);
-					}
-					if (hba->masklen > 0)
-					{
-						if (pg_getnameinfo_all(&hba->mask, hba->masklen,
-											   buffer, sizeof(buffer),
-											   NULL, 0,
-											   NI_NUMERICHOST) == 0)
-							clean_ipv6_addr(hba->mask.ss_family, buffer);
-						maskstr = pstrdup(buffer);
-					}
-				}
-				break;
-			case ipCmpAll:
-				addrstr = "all";
-				break;
-			case ipCmpSameHost:
-				addrstr = "samehost";
-				break;
-			case ipCmpSameNet:
-				addrstr = "samenet";
-				break;
-		}
-		if (addrstr)
-			values[index++] = CStringGetTextDatum(addrstr);
-		else
-			nulls[index++] = true;
-		if (maskstr)
-			values[index++] = CStringGetTextDatum(maskstr);
-		else
-			nulls[index++] = true;
-
-		/* auth_method */
-		values[index++] = CStringGetTextDatum(hba_authname(hba->auth_method));
-
-		/* options */
-		options = gethba_options(hba);
-		if (options)
-			values[index++] = PointerGetDatum(options);
-		else
-			nulls[index++] = true;
-	}
-	else
-	{
-		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
-	}
-
-	/* error */
-	if (err_msg)
-		values[NUM_PG_HBA_FILE_RULES_ATTS - 1] = CStringGetTextDatum(err_msg);
-	else
-		nulls[NUM_PG_HBA_FILE_RULES_ATTS - 1] = true;
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	tuplestore_puttuple(tuple_store, tuple);
-}
-
-/*
- * Read the pg_hba.conf file and fill the tuplestore with view records.
- */
-static void
-fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
-{
-	FILE	   *file;
-	List	   *hba_lines = NIL;
-	ListCell   *line;
-	MemoryContext linecxt;
-	MemoryContext hbacxt;
-	MemoryContext oldcxt;
-
-	/*
-	 * In the unlikely event that we can't open pg_hba.conf, we throw an
-	 * error, rather than trying to report it via some sort of view entry.
-	 * (Most other error conditions should result in a message in a view
-	 * entry.)
-	 */
-	file = AllocateFile(HbaFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
-
-	linecxt = tokenize_file(HbaFileName, file, &hba_lines, DEBUG3);
-	FreeFile(file);
-
-	/* Now parse all the lines */
-	hbacxt = AllocSetContextCreate(CurrentMemoryContext,
-								   "hba parser context",
-								   ALLOCSET_SMALL_SIZES);
-	oldcxt = MemoryContextSwitchTo(hbacxt);
-	foreach(line, hba_lines)
-	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line);
-		HbaLine    *hbaline = NULL;
-
-		/* don't parse lines that already have errors */
-		if (tok_line->err_msg == NULL)
-			hbaline = parse_hba_line(tok_line, DEBUG3);
-
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
-	}
-
-	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
-	/* Free parse_hba_line memory */
-	MemoryContextSwitchTo(oldcxt);
-	MemoryContextDelete(hbacxt);
-}
-
-/*
- * SQL-accessible SRF to return all the entries in the pg_hba.conf file.
- */
-Datum
-pg_hba_file_rules(PG_FUNCTION_ARGS)
-{
-	ReturnSetInfo *rsi;
-
-	/*
-	 * Build tuplestore to hold the result rows.  We must use the Materialize
-	 * mode to be safe against HBA file changes while the cursor is open.
-	 * It's also more efficient than having to look up our current position in
-	 * the parsed list every time.
-	 */
-	SetSingleFuncCall(fcinfo, 0);
-
-	/* Fill the tuplestore */
-	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
-	fill_hba_view(rsi->setResult, rsi->setDesc);
-
-	PG_RETURN_NULL();
-}
-
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2735,7 +2315,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * NULL.
  */
 static IdentLine *
-parse_ident_line(TokenizedLine *tok_line)
+parse_ident_line(TokenizedAuthLine *tok_line)
 {
 	int			line_num = tok_line->line_num;
 	ListCell   *field;
@@ -3026,7 +2606,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -3037,7 +2617,7 @@ load_ident(void)
 	oldcxt = MemoryContextSwitchTo(ident_context);
 	foreach(line_cell, ident_lines)
 	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line_cell);
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line_cell);
 
 		/* don't parse lines that already have errors */
 		if (tok_line->err_msg != NULL)
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 41b486bcef..7c722ea2ce 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -42,6 +42,7 @@ OBJS = \
 	geo_ops.o \
 	geo_selfuncs.o \
 	geo_spgist.o \
+	hbafuncs.o \
 	inet_cidr_ntop.o \
 	inet_net_pton.o \
 	int.o \
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
new file mode 100644
index 0000000000..f230bad8b6
--- /dev/null
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -0,0 +1,423 @@
+/*-------------------------------------------------------------------------
+ *
+ * hbafuncs.c
+ *	  Support functions for authentication files SQL views.
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/hbafuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "catalog/objectaddress.h"
+#include "common/ip.h"
+#include "funcapi.h"
+#include "libpq/hba.h"
+#include "miscadmin.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+
+
+static ArrayType *gethba_options(HbaLine *hba);
+static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+						  int lineno, HbaLine *hba, const char *err_msg);
+static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+
+
+/*
+ * This macro specifies the maximum number of authentication options
+ * that are possible with any given authentication method that is supported.
+ * Currently LDAP supports 11, and there are 3 that are not dependent on
+ * the auth method here.  It may not actually be possible to set all of them
+ * at the same time, but we'll set the macro value high enough to be
+ * conservative and avoid warnings from static analysis tools.
+ */
+#define MAX_HBA_OPTIONS 14
+
+/*
+ * Create a text array listing the options specified in the HBA line.
+ * Return NULL if no options are specified.
+ */
+static ArrayType *
+gethba_options(HbaLine *hba)
+{
+	int			noptions;
+	Datum		options[MAX_HBA_OPTIONS];
+
+	noptions = 0;
+
+	if (hba->auth_method == uaGSS || hba->auth_method == uaSSPI)
+	{
+		if (hba->include_realm)
+			options[noptions++] =
+				CStringGetTextDatum("include_realm=true");
+
+		if (hba->krb_realm)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("krb_realm=%s", hba->krb_realm));
+	}
+
+	if (hba->usermap)
+		options[noptions++] =
+			CStringGetTextDatum(psprintf("map=%s", hba->usermap));
+
+	if (hba->clientcert != clientCertOff)
+		options[noptions++] =
+			CStringGetTextDatum(psprintf("clientcert=%s", (hba->clientcert == clientCertCA) ? "verify-ca" : "verify-full"));
+
+	if (hba->pamservice)
+		options[noptions++] =
+			CStringGetTextDatum(psprintf("pamservice=%s", hba->pamservice));
+
+	if (hba->auth_method == uaLDAP)
+	{
+		if (hba->ldapserver)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapserver=%s", hba->ldapserver));
+
+		if (hba->ldapport)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapport=%d", hba->ldapport));
+
+		if (hba->ldaptls)
+			options[noptions++] =
+				CStringGetTextDatum("ldaptls=true");
+
+		if (hba->ldapprefix)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapprefix=%s", hba->ldapprefix));
+
+		if (hba->ldapsuffix)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsuffix=%s", hba->ldapsuffix));
+
+		if (hba->ldapbasedn)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapbasedn=%s", hba->ldapbasedn));
+
+		if (hba->ldapbinddn)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapbinddn=%s", hba->ldapbinddn));
+
+		if (hba->ldapbindpasswd)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapbindpasswd=%s",
+											 hba->ldapbindpasswd));
+
+		if (hba->ldapsearchattribute)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
+											 hba->ldapsearchattribute));
+
+		if (hba->ldapsearchfilter)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
+											 hba->ldapsearchfilter));
+
+		if (hba->ldapscope)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
+	}
+
+	if (hba->auth_method == uaRADIUS)
+	{
+		if (hba->radiusservers_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiusservers=%s", hba->radiusservers_s));
+
+		if (hba->radiussecrets_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiussecrets=%s", hba->radiussecrets_s));
+
+		if (hba->radiusidentifiers_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiusidentifiers=%s", hba->radiusidentifiers_s));
+
+		if (hba->radiusports_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s));
+	}
+
+	/* If you add more options, consider increasing MAX_HBA_OPTIONS. */
+	Assert(noptions <= MAX_HBA_OPTIONS);
+
+	if (noptions > 0)
+		return construct_array(options, noptions, TEXTOID, -1, false, TYPALIGN_INT);
+	else
+		return NULL;
+}
+
+/* Number of columns in pg_hba_file_rules view */
+#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+
+/*
+ * fill_hba_line: build one row of pg_hba_file_rules view, add it to tuplestore
+ *
+ * tuple_store: where to store data
+ * tupdesc: tuple descriptor for the view
+ * lineno: pg_hba.conf line number (must always be valid)
+ * hba: parsed line data (can be NULL, in which case err_msg should be set)
+ * err_msg: error message (NULL if none)
+ *
+ * Note: leaks memory, but we don't care since this is run in a short-lived
+ * memory context.
+ */
+static void
+fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+			  int lineno, HbaLine *hba, const char *err_msg)
+{
+	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
+	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
+	char		buffer[NI_MAXHOST];
+	HeapTuple	tuple;
+	int			index;
+	ListCell   *lc;
+	const char *typestr;
+	const char *addrstr;
+	const char *maskstr;
+	ArrayType  *options;
+
+	Assert(tupdesc->natts == NUM_PG_HBA_FILE_RULES_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (hba != NULL)
+	{
+		/* type */
+		/* Avoid a default: case so compiler will warn about missing cases */
+		typestr = NULL;
+		switch (hba->conntype)
+		{
+			case ctLocal:
+				typestr = "local";
+				break;
+			case ctHost:
+				typestr = "host";
+				break;
+			case ctHostSSL:
+				typestr = "hostssl";
+				break;
+			case ctHostNoSSL:
+				typestr = "hostnossl";
+				break;
+			case ctHostGSS:
+				typestr = "hostgssenc";
+				break;
+			case ctHostNoGSS:
+				typestr = "hostnogssenc";
+				break;
+		}
+		if (typestr)
+			values[index++] = CStringGetTextDatum(typestr);
+		else
+			nulls[index++] = true;
+
+		/* database */
+		if (hba->databases)
+		{
+			/*
+			 * Flatten HbaToken list to string list.  It might seem that we
+			 * should re-quote any quoted tokens, but that has been rejected
+			 * on the grounds that it makes it harder to compare the array
+			 * elements to other system catalogs.  That makes entries like
+			 * "all" or "samerole" formally ambiguous ... but users who name
+			 * databases/roles that way are inflicting their own pain.
+			 */
+			List	   *names = NIL;
+
+			foreach(lc, hba->databases)
+			{
+				HbaToken   *tok = lfirst(lc);
+
+				names = lappend(names, tok->string);
+			}
+			values[index++] = PointerGetDatum(strlist_to_textarray(names));
+		}
+		else
+			nulls[index++] = true;
+
+		/* user */
+		if (hba->roles)
+		{
+			/* Flatten HbaToken list to string list; see comment above */
+			List	   *roles = NIL;
+
+			foreach(lc, hba->roles)
+			{
+				HbaToken   *tok = lfirst(lc);
+
+				roles = lappend(roles, tok->string);
+			}
+			values[index++] = PointerGetDatum(strlist_to_textarray(roles));
+		}
+		else
+			nulls[index++] = true;
+
+		/* address and netmask */
+		/* Avoid a default: case so compiler will warn about missing cases */
+		addrstr = maskstr = NULL;
+		switch (hba->ip_cmp_method)
+		{
+			case ipCmpMask:
+				if (hba->hostname)
+				{
+					addrstr = hba->hostname;
+				}
+				else
+				{
+					/*
+					 * Note: if pg_getnameinfo_all fails, it'll set buffer to
+					 * "???", which we want to return.
+					 */
+					if (hba->addrlen > 0)
+					{
+						if (pg_getnameinfo_all(&hba->addr, hba->addrlen,
+											   buffer, sizeof(buffer),
+											   NULL, 0,
+											   NI_NUMERICHOST) == 0)
+							clean_ipv6_addr(hba->addr.ss_family, buffer);
+						addrstr = pstrdup(buffer);
+					}
+					if (hba->masklen > 0)
+					{
+						if (pg_getnameinfo_all(&hba->mask, hba->masklen,
+											   buffer, sizeof(buffer),
+											   NULL, 0,
+											   NI_NUMERICHOST) == 0)
+							clean_ipv6_addr(hba->mask.ss_family, buffer);
+						maskstr = pstrdup(buffer);
+					}
+				}
+				break;
+			case ipCmpAll:
+				addrstr = "all";
+				break;
+			case ipCmpSameHost:
+				addrstr = "samehost";
+				break;
+			case ipCmpSameNet:
+				addrstr = "samenet";
+				break;
+		}
+		if (addrstr)
+			values[index++] = CStringGetTextDatum(addrstr);
+		else
+			nulls[index++] = true;
+		if (maskstr)
+			values[index++] = CStringGetTextDatum(maskstr);
+		else
+			nulls[index++] = true;
+
+		/* auth_method */
+		values[index++] = CStringGetTextDatum(hba_authname(hba->auth_method));
+
+		/* options */
+		options = gethba_options(hba);
+		if (options)
+			values[index++] = PointerGetDatum(options);
+		else
+			nulls[index++] = true;
+	}
+	else
+	{
+		/* no parsing result, so set relevant fields to nulls */
+		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_HBA_FILE_RULES_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_HBA_FILE_RULES_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * Read the pg_hba.conf file and fill the tuplestore with view records.
+ */
+static void
+fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *hba_lines = NIL;
+	ListCell   *line;
+	MemoryContext linecxt;
+	MemoryContext hbacxt;
+	MemoryContext oldcxt;
+
+	/*
+	 * In the unlikely event that we can't open pg_hba.conf, we throw an
+	 * error, rather than trying to report it via some sort of view entry.
+	 * (Most other error conditions should result in a message in a view
+	 * entry.)
+	 */
+	file = AllocateFile(HbaFileName, "r");
+	if (file == NULL)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration file \"%s\": %m",
+						HbaFileName)));
+
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	FreeFile(file);
+
+	/* Now parse all the lines */
+	hbacxt = AllocSetContextCreate(CurrentMemoryContext,
+								   "hba parser context",
+								   ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hbacxt);
+	foreach(line, hba_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		HbaLine    *hbaline = NULL;
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg == NULL)
+			hbaline = parse_hba_line(tok_line, DEBUG3);
+
+		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
+					  hbaline, tok_line->err_msg);
+	}
+
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	/* Free parse_hba_line memory */
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(hbacxt);
+}
+
+/*
+ * SQL-accessible SRF to return all the entries in the pg_hba.conf file.
+ */
+Datum
+pg_hba_file_rules(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsi;
+
+	/*
+	 * Build tuplestore to hold the result rows.  We must use the Materialize
+	 * mode to be safe against HBA file changes while the cursor is open.
+	 * It's also more efficient than having to look up our current position in
+	 * the parsed list every time.
+	 */
+	SetSingleFuncCall(fcinfo, 0);
+
+	/* Fill the tuplestore */
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+	fill_hba_view(rsi->setResult, rsi->setDesc);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8d9f3821b1..19924dca67 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -122,6 +122,16 @@ typedef struct HbaLine
 	char	   *radiusports_s;
 } HbaLine;
 
+/*
+ * A single string token lexed from a config file, together with whether
+ * the token had been quoted.
+ */
+typedef struct HbaToken
+{
+	char	   *string;
+	bool		quoted;
+} HbaToken;
+
 typedef struct IdentLine
 {
 	int			linenumber;
@@ -132,6 +142,22 @@ typedef struct IdentLine
 	regex_t		re;
 } IdentLine;
 
+/*
+ * TokenizedAuthLine represents one line lexed from a config file.
+ * Each item in the "fields" list is a sub-list of HbaTokens.
+ * We don't emit a TokenizedAuthLine for empty or all-comment lines,
+ * so "fields" is never NIL (nor are any of its sub-lists).
+ * Exception: if an error occurs during tokenization, we might
+ * have fields == NIL, in which case err_msg != NULL.
+ */
+typedef struct TokenizedAuthLine
+{
+	List	   *fields;			/* List of lists of HbaTokens */
+	int			line_num;		/* Line number */
+	char	   *raw_line;		/* Raw line text */
+	char	   *err_msg;		/* Error message if any */
+} TokenizedAuthLine;
+
 /* kluge to avoid including libpq/libpq-be.h here */
 typedef struct Port hbaPort;
 
@@ -142,6 +168,9 @@ extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
 						  bool case_sensitive);
+extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
+extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
+										List **tok_lines, int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index eaf3e7a8d4..109df9dc90 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2694,7 +2694,7 @@ ToastTupleContext
 ToastedAttribute
 TocEntry
 TokenAuxData
-TokenizedLine
+TokenizedAuthLine
 TrackItem
 TransInvalidationInfo
 TransState
-- 
2.33.1

v3-0002-Add-a-pg_ident_file_mappings-view.patchtext/plain; charset=us-asciiDownload
From 050caf9657ceca7251d98476f325fc2d67f6787b Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 17:38:34 +0800
Subject: [PATCH v3 2/4] Add a pg_ident_file_mappings view.

This view is similar to pg_hba_file_rules view, and can be also helpful to help
diagnosing configuration problems.

A following commit will add the possibility to include files in pg_hba and
pg_ident configuration files, which will then make this view even more useful.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml             | 108 ++++++++++++++++++++
 doc/src/sgml/client-auth.sgml          |  10 ++
 doc/src/sgml/func.sgml                 |   5 +-
 src/backend/catalog/system_views.sql   |   6 ++
 src/backend/libpq/hba.c                |  31 +++---
 src/backend/utils/adt/hbafuncs.c       | 136 +++++++++++++++++++++++++
 src/include/catalog/pg_proc.dat        |   7 ++
 src/include/libpq/hba.h                |   1 +
 src/test/regress/expected/rules.out    |   6 ++
 src/test/regress/expected/sysviews.out |   6 ++
 src/test/regress/sql/sysviews.sql      |   2 +
 11 files changed, 302 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 7777d60514..e87f0543d6 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9540,6 +9540,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <entry>summary of client authentication configuration file contents</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link></entry>
+      <entry>summary of client user name mapping configuration file contents</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-indexes"><structname>pg_indexes</structname></link></entry>
       <entry>indexes</entry>
@@ -10533,6 +10538,109 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </para>
  </sect1>
 
+ <sect1 id="view-pg-ident-file-mappings">
+  <title><structname>pg_ident_file_mappings</structname></title>
+
+  <indexterm zone="view-pg-ident-file-mappings">
+   <primary>pg_ident_file_mappings</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_ident_file_mappings</structname> provides a summary
+   of the contents of the client user name mapping configuration file,
+   <link linkend="auth-username-maps"><filename>pg_ident.conf</filename></link>.
+   A row appears in this view for each
+   non-empty, non-comment line in the file, with annotations indicating
+   whether the rule could be applied successfully.
+  </para>
+
+  <para>
+   This view can be helpful for checking whether planned changes in the
+   authentication configuration file will work, or for diagnosing a previous
+   failure.  Note that this view reports on the <emphasis>current</emphasis>
+   contents of the file, not on what was last loaded by the server.
+  </para>
+
+  <para>
+   By default, the <structname>pg_ident_file_mappings</structname> view can be
+   read only by superusers.
+  </para>
+
+  <table>
+   <title><structname>pg_ident_file_mappings</structname> Columns</title> <tgroup
+   cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this rule in <filename>pg_ident.conf</filename>
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>map_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the map
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>sys_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Detected user name of the client
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pg_username</structfield> <type>text</type>
+      </para>
+      <para>
+       Requested PostgreSQL user name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>error</structfield> <type>text</type>
+      </para>
+      <para>
+       If not null, an error message indicating why this line could not be
+       processed
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+  <para>
+   Usually, a row reflecting an incorrect entry will have values for only
+   the <structfield>line_number</structfield> and <structfield>error</structfield> fields.
+  </para>
+
+  <para>
+   See <xref linkend="client-authentication"/> for more information about
+   client authentication configuration.
+  </para>
+ </sect1>
+
  <sect1 id="view-pg-indexes">
   <title><structname>pg_indexes</structname></title>
 
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..142b0affcb 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -896,6 +896,16 @@ mymap   /^(.*)@otherdomain\.com$   guest
    -HUP</literal>) to make it re-read the file.
   </para>
 
+  <para>
+   The system view
+   <link linkend="view-pg-ident-file-mappings"><structname>pg_ident_file_mappings</structname></link>
+   can be helpful for pre-testing changes to the
+   <filename>pg_ident.conf</filename> file, or for diagnosing problems if
+   loading of the file did not have the desired effects.  Rows in the view with
+   non-null <structfield>error</structfield> fields indicate problems in the
+   corresponding lines of the file.
+  </para>
+
   <para>
    A <filename>pg_ident.conf</filename> file that could be used in
    conjunction with the <filename>pg_hba.conf</filename> file in <xref
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8a802fb225..b32cc61886 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25475,8 +25475,9 @@ SELECT collation for ('foo' COLLATE "de_DE");
         sending a <systemitem>SIGHUP</systemitem> signal to the postmaster
         process, which in turn sends <systemitem>SIGHUP</systemitem> to each
         of its children.) You can use the
-        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link> and
-        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> views
+        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link>,
+        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> and
+        <link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link> views
         to check the configuration files for possible errors, before reloading.
        </para></entry>
       </row>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index bb1ac30cd1..90011f2d68 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -607,6 +607,12 @@ CREATE VIEW pg_hba_file_rules AS
 REVOKE ALL ON pg_hba_file_rules FROM PUBLIC;
 REVOKE EXECUTE ON FUNCTION pg_hba_file_rules() FROM PUBLIC;
 
+CREATE VIEW pg_ident_file_mappings AS
+   SELECT * FROM pg_ident_file_mappings() AS A;
+
+REVOKE ALL ON pg_ident_file_mappings FROM PUBLIC;
+REVOKE EXECUTE ON FUNCTION pg_ident_file_mappings() FROM PUBLIC;
+
 CREATE VIEW pg_timezone_abbrevs AS
     SELECT * FROM pg_timezone_abbrevs();
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index f9843a0b30..68136f6244 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -886,25 +886,22 @@ do { \
 } while (0)
 
 /*
- * Macros for handling pg_ident problems.
- * Much as above, but currently the message level is hardwired as LOG
- * and there is no provision for an err_msg string.
+ * Macros for handling pg_ident problems, similar as above.
  *
  * IDENT_FIELD_ABSENT:
- * Log a message and exit the function if the given ident field ListCell is
- * not populated.
+ * Reports when the given ident field ListCell is not populated.
  *
  * IDENT_MULTI_VALUE:
- * Log a message and exit the function if the given ident token List has more
- * than one element.
+ * Reports when the given ident token List has more than one element.
  */
 #define IDENT_FIELD_ABSENT(field) \
 do { \
 	if (!field) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
 						IdentFileName, line_num))); \
+		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
 } while (0)
@@ -912,11 +909,12 @@ do { \
 #define IDENT_MULTI_VALUE(tokens) \
 do { \
 	if (tokens->length > 1) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
 							line_num, IdentFileName))); \
+		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
 } while (0)
@@ -2305,7 +2303,8 @@ load_hba(void)
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
  *
- * If parsing fails, log a message and return NULL.
+ * If parsing fails, log a message at ereport level elevel, store an error
+ * string in tok_line->err_msg and return NULL.
  *
  * If ident_user is a regular expression (ie. begins with a slash), it is
  * compiled and stored in IdentLine structure.
@@ -2314,10 +2313,11 @@ load_hba(void)
  * to have set a memory context that will be reset if this function returns
  * NULL.
  */
-static IdentLine *
-parse_ident_line(TokenizedAuthLine *tok_line)
+IdentLine *
+parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
 	List	   *tokens;
 	HbaToken   *token;
@@ -2371,11 +2371,14 @@ parse_ident_line(TokenizedAuthLine *tok_line)
 			char		errstr[100];
 
 			pg_regerror(r, &parsedline->re, errstr, sizeof(errstr));
-			ereport(LOG,
+			ereport(elevel,
 					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
 					 errmsg("invalid regular expression \"%s\": %s",
 							parsedline->ident_user + 1, errstr)));
 
+			*err_msg = psprintf("invalid regular expression \"%s\": %s",
+							   parsedline->ident_user + 1, errstr);
+
 			pfree(wstr);
 			return NULL;
 		}
@@ -2626,7 +2629,7 @@ load_ident(void)
 			continue;
 		}
 
-		if ((newline = parse_ident_line(tok_line)) == NULL)
+		if ((newline = parse_ident_line(tok_line, LOG)) == NULL)
 		{
 			/* Parse error; remember there's trouble */
 			ok = false;
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index f230bad8b6..75e69383c2 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -28,6 +28,9 @@ static ArrayType *gethba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 						  int lineno, HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int lineno, IdentLine *ident, const char *err_msg);
+static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
 /*
@@ -421,3 +424,136 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 
 	PG_RETURN_NULL();
 }
+
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+
+/*
+ * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
+ * tuplestore
+ *
+ * tuple_store: where to store data
+ * tupdesc: tuple descriptor for the view
+ * lineno: pg_hba.conf line number (must always be valid)
+ * ident: parsed line data (can be NULL, in which case err_msg should be set)
+ * err_msg: error message (NULL if none)
+ *
+ * Note: leaks memory, but we don't care since this is run in a short-lived
+ * memory context.
+ */
+static void
+fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+				int lineno, IdentLine *ident, const char *err_msg)
+{
+	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	HeapTuple	tuple;
+	int			index;
+
+	Assert(tupdesc->natts == NUM_PG_IDENT_FILE_MAPPINGS_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (ident != NULL)
+	{
+		values[index++] = CStringGetTextDatum(ident->usermap);
+		values[index++] = CStringGetTextDatum(ident->ident_user);
+		values[index++] = CStringGetTextDatum(ident->pg_role);
+	}
+	else
+	{
+		/* no parsing result, so set relevant fields to nulls */
+		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * Read the pg_ident.conf file and fill the tuplestore with view records.
+ */
+static void
+fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *ident_lines = NIL;
+	ListCell   *line;
+	MemoryContext linecxt;
+	MemoryContext identcxt;
+	MemoryContext oldcxt;
+
+	/*
+	 * In the unlikely event that we can't open pg_hba.conf, we throw an
+	 * error, rather than trying to report it via some sort of view entry.
+	 * (Most other error conditions should result in a message in a view
+	 * entry.)
+	 */
+	file = AllocateFile(IdentFileName, "r");
+	if (file == NULL)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open usermap file \"%s\": %m",
+						IdentFileName)));
+
+	linecxt = tokenize_auth_file(HbaFileName, file, &ident_lines, DEBUG3);
+	FreeFile(file);
+
+	/* Now parse all the lines */
+	identcxt = AllocSetContextCreate(CurrentMemoryContext,
+								   "ident parser context",
+								   ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(identcxt);
+	foreach(line, ident_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		IdentLine   *identline = NULL;
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg == NULL)
+			identline = parse_ident_line(tok_line, DEBUG3);
+
+		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
+					  tok_line->err_msg);
+	}
+
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	/* Free parse_hba_line memory */
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(identcxt);
+}
+
+/*
+ * SQL-accessible SRF to return all the entries in the pg_ident.conf file.
+ */
+Datum
+pg_ident_file_mappings(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsi;
+
+	/*
+	 * Build tuplestore to hold the result rows.  We must use the Materialize
+	 * mode to be safe against HBA file changes while the cursor is open.
+	 * It's also more efficient than having to look up our current position in
+	 * the parsed list every time.
+	 */
+	SetSingleFuncCall(fcinfo, 0);
+
+	/* Fill the tuplestore */
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+	fill_ident_view(rsi->setResult, rsi->setDesc);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d8e8715ed1..6ccbc9af4c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6115,6 +6115,13 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o}',
   proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
+{ oid => '9556', descr => 'show pg_ident.conf mappings',
+  proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
+  provolatile => 'v', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o}',
+  proargnames => '{line_number,map_name,sys_name,pg_usernamee,error}',
+  prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 19924dca67..fce7db248b 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -169,6 +169,7 @@ extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
 						  bool case_sensitive);
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
+extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
 										List **tok_lines, int elevel);
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index ac468568a1..76a209b717 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1347,6 +1347,12 @@ pg_hba_file_rules| SELECT a.line_number,
     a.options,
     a.error
    FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.line_number,
+    a.map_name,
+    a.sys_name,
+    a.pg_usernamee,
+    a.error
+   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_usernamee, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 442eeb1e3f..31ba549883 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -55,6 +55,12 @@ select count(*) > 0 as ok from pg_hba_file_rules;
  t
 (1 row)
 
+select count(*) >= 0 as ok from pg_ident_file_mappings;
+ ok 
+----
+ t
+(1 row)
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
  ok 
diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql
index 4980f07be2..1148014e47 100644
--- a/src/test/regress/sql/sysviews.sql
+++ b/src/test/regress/sql/sysviews.sql
@@ -28,6 +28,8 @@ select count(*) >= 0 as ok from pg_file_settings;
 -- There will surely be at least one rule
 select count(*) > 0 as ok from pg_hba_file_rules;
 
+select count(*) >= 0 as ok from pg_ident_file_mappings;
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
 
-- 
2.33.1

#14Aleksander Alekseev
aleksander@timescale.com
In reply to: Julien Rouhaud (#13)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi hackers,

The cfbot says that the patch doesn't apply anymore, so here's a v3 with

the

changes mentioned below.

I came across this thread while looking for the patches that need review.

The v3-0001 patch LGTM.

Since v3-0002 adds a new view and alters pg_proc.dat shouldn't it also
increase CATALOG_VERSION_NO? Not sure if we generally do this in the
patches or expect the committer to make the change manually.

Same question regarding v3-0003. Other than that the patch looks OK, but
doesn't seem to add any tests for the new functionality. Do you think it
would be possible to test-cover the file inclusion? Personally I don't
think it's that critical to have these particular tests, but if they can be
added, I think we should do so.

I didn't review v3-0004 since it's marked as PoC and seems to be a separate
feature that is not targeting PG15. I suggest excluding it from the
patchset in order to keep the focus. Consider creating a new thread and a
new CF entry after we deal with v3-0001...v3-0003.

All in all, the patchset seems to be in good shape and I don't have
anything but some little nitpicks. It passes `make installcheck` and I
verified manually that the file inclusion 1) works 2) write proper error
messages to the logfile when the included file doesn't exist or has wrong
permissions.

--
Best regards,
Aleksander Alekseev

#15Aleksander Alekseev
aleksander@timescale.com
In reply to: Aleksander Alekseev (#14)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi hackers,

It passes `make installcheck` ...

`make installcheck-world`, of course. Sorry for the confusion.

--
Best regards,
Aleksander Alekseev

#16Julien Rouhaud
rjuju123@gmail.com
In reply to: Aleksander Alekseev (#14)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Tue, Mar 22, 2022 at 03:21:20PM +0300, Aleksander Alekseev wrote:

The v3-0001 patch LGTM.

Since v3-0002 adds a new view and alters pg_proc.dat shouldn't it also
increase CATALOG_VERSION_NO? Not sure if we generally do this in the
patches or expect the committer to make the change manually.

It's better to no include any catversion bump, otherwise the patches will
rot very fast. Author can mention the need for a catversion bump in the
commit message, and I usually do so but I apparently forgot.

Same question regarding v3-0003. Other than that the patch looks OK, but
doesn't seem to add any tests for the new functionality. Do you think it
would be possible to test-cover the file inclusion? Personally I don't
think it's that critical to have these particular tests, but if they can be
added, I think we should do so.

Yes, as I mentioned in the first email I'm willing to add test coverage. But
this will require TAP tests, and it's likely going to be a bit annoying to make
sure it has decent coverage and works on all platforms, so I'd rather do it
once I know there's at least some basic agreement on the feature and/or the
approach. Unfortunately, until now there was no feedback on that part despite
the activity on the thread. In my experience it's a good sign that this will
get rejected soon, so I still didn't write tests.

I didn't review v3-0004 since it's marked as PoC and seems to be a separate
feature that is not targeting PG15. I suggest excluding it from the
patchset in order to keep the focus. Consider creating a new thread and a
new CF entry after we deal with v3-0001...v3-0003.

This feature was discussed in some old thread when the file inclusion was first
discussed almost a decade ago, and in my understanding it was part of the "must
have" (along with 0002) in order to accept the feature. That's why I added
it, since I'm also willing to work of that if needed, whether before or after
the file inclusion thing.

All in all, the patchset seems to be in good shape and I don't have
anything but some little nitpicks. It passes `make installcheck` and I
verified manually that the file inclusion 1) works 2) write proper error
messages to the logfile when the included file doesn't exist or has wrong
permissions.

Thanks!

#17Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#16)
1 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Mar 22, 2022 at 09:38:00PM +0800, Julien Rouhaud wrote:

On Tue, Mar 22, 2022 at 03:21:20PM +0300, Aleksander Alekseev wrote:

Since v3-0002 adds a new view and alters pg_proc.dat shouldn't it also
increase CATALOG_VERSION_NO? Not sure if we generally do this in the
patches or expect the committer to make the change manually.

It's better to no include any catversion bump, otherwise the patches will
rot very fast. Author can mention the need for a catversion bump in the
commit message, and I usually do so but I apparently forgot.

Yeah, committers take care of that. You would just expose yourself to
more noise in the CF bot for no gain, as a catversion bump is useful
after a patch has been merged so as as users are able to know when a
cluster needs to be pg_upgrade'd or initdb'd because the catalog
created and run are incompatible.

All in all, the patchset seems to be in good shape and I don't have
anything but some little nitpicks. It passes `make installcheck` and I
verified manually that the file inclusion 1) works 2) write proper error
messages to the logfile when the included file doesn't exist or has wrong
permissions.

Thanks!

Pushing forward with 0001 by the end of the CF is the part that has no
controversy IMO, and I have no objections to it. Now, after looking
at this part, I found a few things, as of:
- HbaToken, the set of elements in the lists of TokenizedAuthLine, is
a weird to use as this layer gets used by both pg_hba.conf and
pg_indent.conf before transforming them into each HbaLine and
IdentLine. While making this part of the internals exposed, I think
that we'd better rename that to AuthToken at least. This impacts the
names of some routines internal to hba.c to copy and create
AuthTokens.
- s/gethba_options/get_hba_options/, to be consistent with
fill_hba_view() and other things.
- The comment at the top of tokenize_auth_file() needed a refresh.

That's mostly cosmetic, and the rest of the code moved is identical.
So at the end this part looks rather commitable to me.

I have not been able to test 0002 in details, but it looks rather
rather sane to me at quick glance, and it is simple. The argument
about more TAP tests applies to it, though, even if there is one SQL
test to check the function execution. It is probably better to not
consider 0003 and 0004 for this CF.
--
Michael

Attachments:

v4-0001-Extract-view-processing-code-from-hba.c.patchtext/x-diff; charset=us-asciiDownload
From dbee2a5b673b3bac2e4df0ec966912c29416fc2c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 23 Mar 2022 10:56:15 +0900
Subject: [PATCH v4] Extract view processing code from hba.c

This file is already quite big and a following commit will add yet an
additional view, so let's move all the view related code in hba.c into a new
adt/hbafuncs.c.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/libpq/hba.h          |  31 ++
 src/backend/libpq/hba.c          | 517 +++----------------------------
 src/backend/utils/adt/Makefile   |   1 +
 src/backend/utils/adt/hbafuncs.c | 428 +++++++++++++++++++++++++
 src/tools/pgindent/typedefs.list |   4 +-
 5 files changed, 511 insertions(+), 470 deletions(-)
 create mode 100644 src/backend/utils/adt/hbafuncs.c

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 8d9f3821b1..13ecb329f8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -132,6 +132,34 @@ typedef struct IdentLine
 	regex_t		re;
 } IdentLine;
 
+/*
+ * A single string token lexed from an authentication configuration file
+ * (pg_ident.conf or pg_hba.conf), together with whether the token has
+ * been quoted.
+ */
+typedef struct AuthToken
+{
+	char	   *string;
+	bool		quoted;
+} AuthToken;
+
+/*
+ * TokenizedAuthLine represents one line lexed from an authentication
+ * configuration file.  Each item in the "fields" list is a sub-list of
+ * AuthTokens.  We don't emit a TokenizedAuthLine for empty or all-comment
+ * lines, so "fields" is never NIL (nor are any of its sub-lists).
+ *
+ * Exception: if an error occurs during tokenization, we might have
+ * fields == NIL, in which case err_msg != NULL.
+ */
+typedef struct TokenizedAuthLine
+{
+	List	   *fields;			/* List of lists of AuthTokens */
+	int			line_num;		/* Line number */
+	char	   *raw_line;		/* Raw line text */
+	char	   *err_msg;		/* Error message if any */
+} TokenizedAuthLine;
+
 /* kluge to avoid including libpq/libpq-be.h here */
 typedef struct Port hbaPort;
 
@@ -142,6 +170,9 @@ extern void hba_getauthmethod(hbaPort *port);
 extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
 						  bool case_sensitive);
+extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
+extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
+										List **tok_lines, int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 90953c38f3..673135144d 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -68,32 +68,6 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
-/*
- * A single string token lexed from a config file, together with whether
- * the token had been quoted.
- */
-typedef struct HbaToken
-{
-	char	   *string;
-	bool		quoted;
-} HbaToken;
-
-/*
- * TokenizedLine represents one line lexed from a config file.
- * Each item in the "fields" list is a sub-list of HbaTokens.
- * We don't emit a TokenizedLine for empty or all-comment lines,
- * so "fields" is never NIL (nor are any of its sub-lists).
- * Exception: if an error occurs during tokenization, we might
- * have fields == NIL, in which case err_msg != NULL.
- */
-typedef struct TokenizedLine
-{
-	List	   *fields;			/* List of lists of HbaTokens */
-	int			line_num;		/* Line number */
-	char	   *raw_line;		/* Raw line text */
-	char	   *err_msg;		/* Error message if any */
-} TokenizedLine;
-
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -138,16 +112,10 @@ static const char *const UserAuthName[] =
 };
 
 
-static MemoryContext tokenize_file(const char *filename, FILE *file,
-								   List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
-static ArrayType *gethba_options(HbaLine *hba);
-static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
-static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
 /*
@@ -288,31 +256,31 @@ next_token(char **lineptr, char *buf, int bufsz,
 }
 
 /*
- * Construct a palloc'd HbaToken struct, copying the given string.
+ * Construct a palloc'd AuthToken struct, copying the given string.
  */
-static HbaToken *
-make_hba_token(const char *token, bool quoted)
+static AuthToken *
+make_auth_token(const char *token, bool quoted)
 {
-	HbaToken   *hbatoken;
+	AuthToken  *authtoken;
 	int			toklen;
 
 	toklen = strlen(token);
 	/* we copy string into same palloc block as the struct */
-	hbatoken = (HbaToken *) palloc(sizeof(HbaToken) + toklen + 1);
-	hbatoken->string = (char *) hbatoken + sizeof(HbaToken);
-	hbatoken->quoted = quoted;
-	memcpy(hbatoken->string, token, toklen + 1);
+	authtoken = (AuthToken *) palloc(sizeof(AuthToken) + toklen + 1);
+	authtoken->string = (char *) authtoken + sizeof(AuthToken);
+	authtoken->quoted = quoted;
+	memcpy(authtoken->string, token, toklen + 1);
 
-	return hbatoken;
+	return authtoken;
 }
 
 /*
- * Copy a HbaToken struct into freshly palloc'd memory.
+ * Copy a AuthToken struct into freshly palloc'd memory.
  */
-static HbaToken *
-copy_hba_token(HbaToken *in)
+static AuthToken *
+copy_auth_token(AuthToken *in)
 {
-	HbaToken   *out = make_hba_token(in->string, in->quoted);
+	AuthToken  *out = make_auth_token(in->string, in->quoted);
 
 	return out;
 }
@@ -329,7 +297,7 @@ copy_hba_token(HbaToken *in)
  * may be non-NIL anyway, so *err_msg must be tested to determine whether
  * there was an error.
  *
- * The result is a List of HbaToken structs, one for each token in the field,
+ * The result is a List of AuthToken structs, one for each token in the field,
  * or NIL if we reached EOL.
  */
 static List *
@@ -353,7 +321,7 @@ next_field_expand(const char *filename, char **lineptr,
 			tokens = tokenize_inc_file(tokens, filename, buf + 1,
 									   elevel, err_msg);
 		else
-			tokens = lappend(tokens, make_hba_token(buf, initial_quote));
+			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
 	} while (trailing_comma && (*err_msg == NULL));
 
 	return tokens;
@@ -364,7 +332,7 @@ next_field_expand(const char *filename, char **lineptr,
  *		Expand a file included from another file into an hba "field"
  *
  * Opens and tokenises a file included from another HBA config file with @,
- * and returns all values found therein as a flat list of HbaTokens.  If a
+ * and returns all values found therein as a flat list of AuthTokens.  If a
  * @-token is found, recursively expand it.  The newly read tokens are
  * appended to "tokens" (so that foo,bar,@baz does what you expect).
  * All new tokens are allocated in caller's memory context.
@@ -419,7 +387,7 @@ tokenize_inc_file(List *tokens,
 	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -427,7 +395,7 @@ tokenize_inc_file(List *tokens,
 	/* Copy all tokens found in the file and append to the tokens list */
 	foreach(inc_line, inc_lines)
 	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(inc_line);
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(inc_line);
 		ListCell   *inc_field;
 
 		/* If any line has an error, propagate that up to caller */
@@ -444,9 +412,9 @@ tokenize_inc_file(List *tokens,
 
 			foreach(inc_token, inc_tokens)
 			{
-				HbaToken   *token = lfirst(inc_token);
+				AuthToken  *token = lfirst(inc_token);
 
-				tokens = lappend(tokens, copy_hba_token(token));
+				tokens = lappend(tokens, copy_auth_token(token));
 			}
 		}
 	}
@@ -456,9 +424,11 @@ tokenize_inc_file(List *tokens,
 }
 
 /*
- * Tokenize the given file.
+ * tokenize_auth_file
+ *		Tokenize the given file.
  *
- * The output is a list of TokenizedLine structs; see struct definition above.
+ * The output is a list of TokenizedAuthLine structs; see the struct definition
+ * in libpq/hba.h.
  *
  * filename: the absolute path to the target file
  * file: the already-opened target file
@@ -466,14 +436,15 @@ tokenize_inc_file(List *tokens,
  * elevel: message logging level
  *
  * Errors are reported by logging messages at ereport level elevel and by
- * adding TokenizedLine structs containing non-null err_msg fields to the
+ * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
  *
  * Return value is a memory context which contains all memory allocated by
  * this function (it's a child of caller's context).
  */
-static MemoryContext
-tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int elevel)
 {
 	int			line_number = 1;
 	StringInfoData buf;
@@ -481,7 +452,7 @@ tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
 	MemoryContext oldcxt;
 
 	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_file",
+									"tokenize_auth_file",
 									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
@@ -550,12 +521,14 @@ tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel)
 				current_line = lappend(current_line, current_field);
 		}
 
-		/* Reached EOL; emit line to TokenizedLine list unless it's boring */
+		/*
+		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 */
 		if (current_line != NIL || err_msg != NULL)
 		{
-			TokenizedLine *tok_line;
+			TokenizedAuthLine *tok_line;
 
-			tok_line = (TokenizedLine *) palloc(sizeof(TokenizedLine));
+			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
 			tok_line->fields = current_line;
 			tok_line->line_num = line_number;
 			tok_line->raw_line = pstrdup(buf.data);
@@ -600,13 +573,13 @@ is_member(Oid userid, const char *role)
 }
 
 /*
- * Check HbaToken list for a match to role, allowing group names.
+ * Check AuthToken list for a match to role, allowing group names.
  */
 static bool
 check_role(const char *role, Oid roleid, List *tokens)
 {
 	ListCell   *cell;
-	HbaToken   *tok;
+	AuthToken  *tok;
 
 	foreach(cell, tokens)
 	{
@@ -624,13 +597,13 @@ check_role(const char *role, Oid roleid, List *tokens)
 }
 
 /*
- * Check to see if db/role combination matches HbaToken list.
+ * Check to see if db/role combination matches AuthToken list.
  */
 static bool
 check_db(const char *dbname, const char *role, Oid roleid, List *tokens)
 {
 	ListCell   *cell;
-	HbaToken   *tok;
+	AuthToken  *tok;
 
 	foreach(cell, tokens)
 	{
@@ -962,8 +935,8 @@ do { \
  * to have set a memory context that will be reset if this function returns
  * NULL.
  */
-static HbaLine *
-parse_hba_line(TokenizedLine *tok_line, int elevel)
+HbaLine *
+parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
 	char	  **err_msg = &tok_line->err_msg;
@@ -976,7 +949,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 	ListCell   *field;
 	List	   *tokens;
 	ListCell   *tokencell;
-	HbaToken   *token;
+	AuthToken  *token;
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
@@ -1097,7 +1070,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 	foreach(tokencell, tokens)
 	{
 		parsedline->databases = lappend(parsedline->databases,
-										copy_hba_token(lfirst(tokencell)));
+										copy_auth_token(lfirst(tokencell)));
 	}
 
 	/* Get the roles. */
@@ -1117,7 +1090,7 @@ parse_hba_line(TokenizedLine *tok_line, int elevel)
 	foreach(tokencell, tokens)
 	{
 		parsedline->roles = lappend(parsedline->roles,
-									copy_hba_token(lfirst(tokencell)));
+									copy_auth_token(lfirst(tokencell)));
 	}
 
 	if (parsedline->conntype != ctLocal)
@@ -2257,7 +2230,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2268,7 +2241,7 @@ load_hba(void)
 	oldcxt = MemoryContextSwitchTo(hbacxt);
 	foreach(line, hba_lines)
 	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line);
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
 		HbaLine    *newline;
 
 		/* don't parse lines that already have errors */
@@ -2328,398 +2301,6 @@ load_hba(void)
 	return true;
 }
 
-/*
- * This macro specifies the maximum number of authentication options
- * that are possible with any given authentication method that is supported.
- * Currently LDAP supports 11, and there are 3 that are not dependent on
- * the auth method here.  It may not actually be possible to set all of them
- * at the same time, but we'll set the macro value high enough to be
- * conservative and avoid warnings from static analysis tools.
- */
-#define MAX_HBA_OPTIONS 14
-
-/*
- * Create a text array listing the options specified in the HBA line.
- * Return NULL if no options are specified.
- */
-static ArrayType *
-gethba_options(HbaLine *hba)
-{
-	int			noptions;
-	Datum		options[MAX_HBA_OPTIONS];
-
-	noptions = 0;
-
-	if (hba->auth_method == uaGSS || hba->auth_method == uaSSPI)
-	{
-		if (hba->include_realm)
-			options[noptions++] =
-				CStringGetTextDatum("include_realm=true");
-
-		if (hba->krb_realm)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("krb_realm=%s", hba->krb_realm));
-	}
-
-	if (hba->usermap)
-		options[noptions++] =
-			CStringGetTextDatum(psprintf("map=%s", hba->usermap));
-
-	if (hba->clientcert != clientCertOff)
-		options[noptions++] =
-			CStringGetTextDatum(psprintf("clientcert=%s", (hba->clientcert == clientCertCA) ? "verify-ca" : "verify-full"));
-
-	if (hba->pamservice)
-		options[noptions++] =
-			CStringGetTextDatum(psprintf("pamservice=%s", hba->pamservice));
-
-	if (hba->auth_method == uaLDAP)
-	{
-		if (hba->ldapserver)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapserver=%s", hba->ldapserver));
-
-		if (hba->ldapport)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapport=%d", hba->ldapport));
-
-		if (hba->ldaptls)
-			options[noptions++] =
-				CStringGetTextDatum("ldaptls=true");
-
-		if (hba->ldapprefix)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapprefix=%s", hba->ldapprefix));
-
-		if (hba->ldapsuffix)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapsuffix=%s", hba->ldapsuffix));
-
-		if (hba->ldapbasedn)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapbasedn=%s", hba->ldapbasedn));
-
-		if (hba->ldapbinddn)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapbinddn=%s", hba->ldapbinddn));
-
-		if (hba->ldapbindpasswd)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapbindpasswd=%s",
-											 hba->ldapbindpasswd));
-
-		if (hba->ldapsearchattribute)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
-											 hba->ldapsearchattribute));
-
-		if (hba->ldapsearchfilter)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
-											 hba->ldapsearchfilter));
-
-		if (hba->ldapscope)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
-	}
-
-	if (hba->auth_method == uaRADIUS)
-	{
-		if (hba->radiusservers_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiusservers=%s", hba->radiusservers_s));
-
-		if (hba->radiussecrets_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiussecrets=%s", hba->radiussecrets_s));
-
-		if (hba->radiusidentifiers_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiusidentifiers=%s", hba->radiusidentifiers_s));
-
-		if (hba->radiusports_s)
-			options[noptions++] =
-				CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s));
-	}
-
-	/* If you add more options, consider increasing MAX_HBA_OPTIONS. */
-	Assert(noptions <= MAX_HBA_OPTIONS);
-
-	if (noptions > 0)
-		return construct_array(options, noptions, TEXTOID, -1, false, TYPALIGN_INT);
-	else
-		return NULL;
-}
-
-/* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
-
-/*
- * fill_hba_line: build one row of pg_hba_file_rules view, add it to tuplestore
- *
- * tuple_store: where to store data
- * tupdesc: tuple descriptor for the view
- * lineno: pg_hba.conf line number (must always be valid)
- * hba: parsed line data (can be NULL, in which case err_msg should be set)
- * err_msg: error message (NULL if none)
- *
- * Note: leaks memory, but we don't care since this is run in a short-lived
- * memory context.
- */
-static void
-fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
-{
-	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
-	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
-	char		buffer[NI_MAXHOST];
-	HeapTuple	tuple;
-	int			index;
-	ListCell   *lc;
-	const char *typestr;
-	const char *addrstr;
-	const char *maskstr;
-	ArrayType  *options;
-
-	Assert(tupdesc->natts == NUM_PG_HBA_FILE_RULES_ATTS);
-
-	memset(values, 0, sizeof(values));
-	memset(nulls, 0, sizeof(nulls));
-	index = 0;
-
-	/* line_number */
-	values[index++] = Int32GetDatum(lineno);
-
-	if (hba != NULL)
-	{
-		/* type */
-		/* Avoid a default: case so compiler will warn about missing cases */
-		typestr = NULL;
-		switch (hba->conntype)
-		{
-			case ctLocal:
-				typestr = "local";
-				break;
-			case ctHost:
-				typestr = "host";
-				break;
-			case ctHostSSL:
-				typestr = "hostssl";
-				break;
-			case ctHostNoSSL:
-				typestr = "hostnossl";
-				break;
-			case ctHostGSS:
-				typestr = "hostgssenc";
-				break;
-			case ctHostNoGSS:
-				typestr = "hostnogssenc";
-				break;
-		}
-		if (typestr)
-			values[index++] = CStringGetTextDatum(typestr);
-		else
-			nulls[index++] = true;
-
-		/* database */
-		if (hba->databases)
-		{
-			/*
-			 * Flatten HbaToken list to string list.  It might seem that we
-			 * should re-quote any quoted tokens, but that has been rejected
-			 * on the grounds that it makes it harder to compare the array
-			 * elements to other system catalogs.  That makes entries like
-			 * "all" or "samerole" formally ambiguous ... but users who name
-			 * databases/roles that way are inflicting their own pain.
-			 */
-			List	   *names = NIL;
-
-			foreach(lc, hba->databases)
-			{
-				HbaToken   *tok = lfirst(lc);
-
-				names = lappend(names, tok->string);
-			}
-			values[index++] = PointerGetDatum(strlist_to_textarray(names));
-		}
-		else
-			nulls[index++] = true;
-
-		/* user */
-		if (hba->roles)
-		{
-			/* Flatten HbaToken list to string list; see comment above */
-			List	   *roles = NIL;
-
-			foreach(lc, hba->roles)
-			{
-				HbaToken   *tok = lfirst(lc);
-
-				roles = lappend(roles, tok->string);
-			}
-			values[index++] = PointerGetDatum(strlist_to_textarray(roles));
-		}
-		else
-			nulls[index++] = true;
-
-		/* address and netmask */
-		/* Avoid a default: case so compiler will warn about missing cases */
-		addrstr = maskstr = NULL;
-		switch (hba->ip_cmp_method)
-		{
-			case ipCmpMask:
-				if (hba->hostname)
-				{
-					addrstr = hba->hostname;
-				}
-				else
-				{
-					/*
-					 * Note: if pg_getnameinfo_all fails, it'll set buffer to
-					 * "???", which we want to return.
-					 */
-					if (hba->addrlen > 0)
-					{
-						if (pg_getnameinfo_all(&hba->addr, hba->addrlen,
-											   buffer, sizeof(buffer),
-											   NULL, 0,
-											   NI_NUMERICHOST) == 0)
-							clean_ipv6_addr(hba->addr.ss_family, buffer);
-						addrstr = pstrdup(buffer);
-					}
-					if (hba->masklen > 0)
-					{
-						if (pg_getnameinfo_all(&hba->mask, hba->masklen,
-											   buffer, sizeof(buffer),
-											   NULL, 0,
-											   NI_NUMERICHOST) == 0)
-							clean_ipv6_addr(hba->mask.ss_family, buffer);
-						maskstr = pstrdup(buffer);
-					}
-				}
-				break;
-			case ipCmpAll:
-				addrstr = "all";
-				break;
-			case ipCmpSameHost:
-				addrstr = "samehost";
-				break;
-			case ipCmpSameNet:
-				addrstr = "samenet";
-				break;
-		}
-		if (addrstr)
-			values[index++] = CStringGetTextDatum(addrstr);
-		else
-			nulls[index++] = true;
-		if (maskstr)
-			values[index++] = CStringGetTextDatum(maskstr);
-		else
-			nulls[index++] = true;
-
-		/* auth_method */
-		values[index++] = CStringGetTextDatum(hba_authname(hba->auth_method));
-
-		/* options */
-		options = gethba_options(hba);
-		if (options)
-			values[index++] = PointerGetDatum(options);
-		else
-			nulls[index++] = true;
-	}
-	else
-	{
-		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
-	}
-
-	/* error */
-	if (err_msg)
-		values[NUM_PG_HBA_FILE_RULES_ATTS - 1] = CStringGetTextDatum(err_msg);
-	else
-		nulls[NUM_PG_HBA_FILE_RULES_ATTS - 1] = true;
-
-	tuple = heap_form_tuple(tupdesc, values, nulls);
-	tuplestore_puttuple(tuple_store, tuple);
-}
-
-/*
- * Read the pg_hba.conf file and fill the tuplestore with view records.
- */
-static void
-fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
-{
-	FILE	   *file;
-	List	   *hba_lines = NIL;
-	ListCell   *line;
-	MemoryContext linecxt;
-	MemoryContext hbacxt;
-	MemoryContext oldcxt;
-
-	/*
-	 * In the unlikely event that we can't open pg_hba.conf, we throw an
-	 * error, rather than trying to report it via some sort of view entry.
-	 * (Most other error conditions should result in a message in a view
-	 * entry.)
-	 */
-	file = AllocateFile(HbaFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
-
-	linecxt = tokenize_file(HbaFileName, file, &hba_lines, DEBUG3);
-	FreeFile(file);
-
-	/* Now parse all the lines */
-	hbacxt = AllocSetContextCreate(CurrentMemoryContext,
-								   "hba parser context",
-								   ALLOCSET_SMALL_SIZES);
-	oldcxt = MemoryContextSwitchTo(hbacxt);
-	foreach(line, hba_lines)
-	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line);
-		HbaLine    *hbaline = NULL;
-
-		/* don't parse lines that already have errors */
-		if (tok_line->err_msg == NULL)
-			hbaline = parse_hba_line(tok_line, DEBUG3);
-
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
-	}
-
-	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
-	/* Free parse_hba_line memory */
-	MemoryContextSwitchTo(oldcxt);
-	MemoryContextDelete(hbacxt);
-}
-
-/*
- * SQL-accessible SRF to return all the entries in the pg_hba.conf file.
- */
-Datum
-pg_hba_file_rules(PG_FUNCTION_ARGS)
-{
-	ReturnSetInfo *rsi;
-
-	/*
-	 * Build tuplestore to hold the result rows.  We must use the Materialize
-	 * mode to be safe against HBA file changes while the cursor is open.
-	 * It's also more efficient than having to look up our current position in
-	 * the parsed list every time.
-	 */
-	SetSingleFuncCall(fcinfo, 0);
-
-	/* Fill the tuplestore */
-	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
-	fill_hba_view(rsi->setResult, rsi->setDesc);
-
-	PG_RETURN_NULL();
-}
-
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2735,12 +2316,12 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * NULL.
  */
 static IdentLine *
-parse_ident_line(TokenizedLine *tok_line)
+parse_ident_line(TokenizedAuthLine *tok_line)
 {
 	int			line_num = tok_line->line_num;
 	ListCell   *field;
 	List	   *tokens;
-	HbaToken   *token;
+	AuthToken  *token;
 	IdentLine  *parsedline;
 
 	Assert(tok_line->fields != NIL);
@@ -3026,7 +2607,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -3037,7 +2618,7 @@ load_ident(void)
 	oldcxt = MemoryContextSwitchTo(ident_context);
 	foreach(line_cell, ident_lines)
 	{
-		TokenizedLine *tok_line = (TokenizedLine *) lfirst(line_cell);
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line_cell);
 
 		/* don't parse lines that already have errors */
 		if (tok_line->err_msg != NULL)
diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile
index 41b486bcef..7c722ea2ce 100644
--- a/src/backend/utils/adt/Makefile
+++ b/src/backend/utils/adt/Makefile
@@ -42,6 +42,7 @@ OBJS = \
 	geo_ops.o \
 	geo_selfuncs.o \
 	geo_spgist.o \
+	hbafuncs.o \
 	inet_cidr_ntop.o \
 	inet_net_pton.o \
 	int.o \
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
new file mode 100644
index 0000000000..f46cd935a1
--- /dev/null
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -0,0 +1,428 @@
+/*-------------------------------------------------------------------------
+ *
+ * hbafuncs.c
+ *	  Support functions for SQL views of authentication files.
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/adt/hbafuncs.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "catalog/objectaddress.h"
+#include "common/ip.h"
+#include "funcapi.h"
+#include "libpq/hba.h"
+#include "miscadmin.h"
+#include "utils/array.h"
+#include "utils/builtins.h"
+#include "utils/guc.h"
+
+
+static ArrayType *get_hba_options(HbaLine *hba);
+static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+						  int lineno, HbaLine *hba, const char *err_msg);
+static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+
+
+/*
+ * This macro specifies the maximum number of authentication options
+ * that are possible with any given authentication method that is supported.
+ * Currently LDAP supports 11, and there are 3 that are not dependent on
+ * the auth method here.  It may not actually be possible to set all of them
+ * at the same time, but we'll set the macro value high enough to be
+ * conservative and avoid warnings from static analysis tools.
+ */
+#define MAX_HBA_OPTIONS 14
+
+/*
+ * Create a text array listing the options specified in the HBA line.
+ * Return NULL if no options are specified.
+ */
+static ArrayType *
+get_hba_options(HbaLine *hba)
+{
+	int			noptions;
+	Datum		options[MAX_HBA_OPTIONS];
+
+	noptions = 0;
+
+	if (hba->auth_method == uaGSS || hba->auth_method == uaSSPI)
+	{
+		if (hba->include_realm)
+			options[noptions++] =
+				CStringGetTextDatum("include_realm=true");
+
+		if (hba->krb_realm)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("krb_realm=%s", hba->krb_realm));
+	}
+
+	if (hba->usermap)
+		options[noptions++] =
+			CStringGetTextDatum(psprintf("map=%s", hba->usermap));
+
+	if (hba->clientcert != clientCertOff)
+		options[noptions++] =
+			CStringGetTextDatum(psprintf("clientcert=%s", (hba->clientcert == clientCertCA) ? "verify-ca" : "verify-full"));
+
+	if (hba->pamservice)
+		options[noptions++] =
+			CStringGetTextDatum(psprintf("pamservice=%s", hba->pamservice));
+
+	if (hba->auth_method == uaLDAP)
+	{
+		if (hba->ldapserver)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapserver=%s", hba->ldapserver));
+
+		if (hba->ldapport)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapport=%d", hba->ldapport));
+
+		if (hba->ldaptls)
+			options[noptions++] =
+				CStringGetTextDatum("ldaptls=true");
+
+		if (hba->ldapprefix)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapprefix=%s", hba->ldapprefix));
+
+		if (hba->ldapsuffix)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsuffix=%s", hba->ldapsuffix));
+
+		if (hba->ldapbasedn)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapbasedn=%s", hba->ldapbasedn));
+
+		if (hba->ldapbinddn)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapbinddn=%s", hba->ldapbinddn));
+
+		if (hba->ldapbindpasswd)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapbindpasswd=%s",
+											 hba->ldapbindpasswd));
+
+		if (hba->ldapsearchattribute)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsearchattribute=%s",
+											 hba->ldapsearchattribute));
+
+		if (hba->ldapsearchfilter)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapsearchfilter=%s",
+											 hba->ldapsearchfilter));
+
+		if (hba->ldapscope)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope));
+	}
+
+	if (hba->auth_method == uaRADIUS)
+	{
+		if (hba->radiusservers_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiusservers=%s", hba->radiusservers_s));
+
+		if (hba->radiussecrets_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiussecrets=%s", hba->radiussecrets_s));
+
+		if (hba->radiusidentifiers_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiusidentifiers=%s", hba->radiusidentifiers_s));
+
+		if (hba->radiusports_s)
+			options[noptions++] =
+				CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s));
+	}
+
+	/* If you add more options, consider increasing MAX_HBA_OPTIONS. */
+	Assert(noptions <= MAX_HBA_OPTIONS);
+
+	if (noptions > 0)
+		return construct_array(options, noptions, TEXTOID, -1, false, TYPALIGN_INT);
+	else
+		return NULL;
+}
+
+/* Number of columns in pg_hba_file_rules view */
+#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+
+/*
+ * fill_hba_line
+ *		Build one row of pg_hba_file_rules view, add it to tuplestore.
+ *
+ * tuple_store: where to store data
+ * tupdesc: tuple descriptor for the view
+ * lineno: pg_hba.conf line number (must always be valid)
+ * hba: parsed line data (can be NULL, in which case err_msg should be set)
+ * err_msg: error message (NULL if none)
+ *
+ * Note: leaks memory, but we don't care since this is run in a short-lived
+ * memory context.
+ */
+static void
+fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+			  int lineno, HbaLine *hba, const char *err_msg)
+{
+	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
+	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
+	char		buffer[NI_MAXHOST];
+	HeapTuple	tuple;
+	int			index;
+	ListCell   *lc;
+	const char *typestr;
+	const char *addrstr;
+	const char *maskstr;
+	ArrayType  *options;
+
+	Assert(tupdesc->natts == NUM_PG_HBA_FILE_RULES_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (hba != NULL)
+	{
+		/* type */
+		/* Avoid a default: case so compiler will warn about missing cases */
+		typestr = NULL;
+		switch (hba->conntype)
+		{
+			case ctLocal:
+				typestr = "local";
+				break;
+			case ctHost:
+				typestr = "host";
+				break;
+			case ctHostSSL:
+				typestr = "hostssl";
+				break;
+			case ctHostNoSSL:
+				typestr = "hostnossl";
+				break;
+			case ctHostGSS:
+				typestr = "hostgssenc";
+				break;
+			case ctHostNoGSS:
+				typestr = "hostnogssenc";
+				break;
+		}
+		if (typestr)
+			values[index++] = CStringGetTextDatum(typestr);
+		else
+			nulls[index++] = true;
+
+		/* database */
+		if (hba->databases)
+		{
+			/*
+			 * Flatten AuthToken list to string list.  It might seem that we
+			 * should re-quote any quoted tokens, but that has been rejected
+			 * on the grounds that it makes it harder to compare the array
+			 * elements to other system catalogs.  That makes entries like
+			 * "all" or "samerole" formally ambiguous ... but users who name
+			 * databases/roles that way are inflicting their own pain.
+			 */
+			List	   *names = NIL;
+
+			foreach(lc, hba->databases)
+			{
+				AuthToken  *tok = lfirst(lc);
+
+				names = lappend(names, tok->string);
+			}
+			values[index++] = PointerGetDatum(strlist_to_textarray(names));
+		}
+		else
+			nulls[index++] = true;
+
+		/* user */
+		if (hba->roles)
+		{
+			/* Flatten AuthToken list to string list; see comment above */
+			List	   *roles = NIL;
+
+			foreach(lc, hba->roles)
+			{
+				AuthToken  *tok = lfirst(lc);
+
+				roles = lappend(roles, tok->string);
+			}
+			values[index++] = PointerGetDatum(strlist_to_textarray(roles));
+		}
+		else
+			nulls[index++] = true;
+
+		/* address and netmask */
+		/* Avoid a default: case so compiler will warn about missing cases */
+		addrstr = maskstr = NULL;
+		switch (hba->ip_cmp_method)
+		{
+			case ipCmpMask:
+				if (hba->hostname)
+				{
+					addrstr = hba->hostname;
+				}
+				else
+				{
+					/*
+					 * Note: if pg_getnameinfo_all fails, it'll set buffer to
+					 * "???", which we want to return.
+					 */
+					if (hba->addrlen > 0)
+					{
+						if (pg_getnameinfo_all(&hba->addr, hba->addrlen,
+											   buffer, sizeof(buffer),
+											   NULL, 0,
+											   NI_NUMERICHOST) == 0)
+							clean_ipv6_addr(hba->addr.ss_family, buffer);
+						addrstr = pstrdup(buffer);
+					}
+					if (hba->masklen > 0)
+					{
+						if (pg_getnameinfo_all(&hba->mask, hba->masklen,
+											   buffer, sizeof(buffer),
+											   NULL, 0,
+											   NI_NUMERICHOST) == 0)
+							clean_ipv6_addr(hba->mask.ss_family, buffer);
+						maskstr = pstrdup(buffer);
+					}
+				}
+				break;
+			case ipCmpAll:
+				addrstr = "all";
+				break;
+			case ipCmpSameHost:
+				addrstr = "samehost";
+				break;
+			case ipCmpSameNet:
+				addrstr = "samenet";
+				break;
+		}
+		if (addrstr)
+			values[index++] = CStringGetTextDatum(addrstr);
+		else
+			nulls[index++] = true;
+		if (maskstr)
+			values[index++] = CStringGetTextDatum(maskstr);
+		else
+			nulls[index++] = true;
+
+		/* auth_method */
+		values[index++] = CStringGetTextDatum(hba_authname(hba->auth_method));
+
+		/* options */
+		options = get_hba_options(hba);
+		if (options)
+			values[index++] = PointerGetDatum(options);
+		else
+			nulls[index++] = true;
+	}
+	else
+	{
+		/* no parsing result, so set relevant fields to nulls */
+		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_HBA_FILE_RULES_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_HBA_FILE_RULES_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * fill_hba_view
+ *		Read the pg_hba.conf file and fill the tuplestore with view records.
+ */
+static void
+fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *hba_lines = NIL;
+	ListCell   *line;
+	MemoryContext linecxt;
+	MemoryContext hbacxt;
+	MemoryContext oldcxt;
+
+	/*
+	 * In the unlikely event that we can't open pg_hba.conf, we throw an
+	 * error, rather than trying to report it via some sort of view entry.
+	 * (Most other error conditions should result in a message in a view
+	 * entry.)
+	 */
+	file = AllocateFile(HbaFileName, "r");
+	if (file == NULL)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration file \"%s\": %m",
+						HbaFileName)));
+
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	FreeFile(file);
+
+	/* Now parse all the lines */
+	hbacxt = AllocSetContextCreate(CurrentMemoryContext,
+								   "hba parser context",
+								   ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(hbacxt);
+	foreach(line, hba_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		HbaLine    *hbaline = NULL;
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg == NULL)
+			hbaline = parse_hba_line(tok_line, DEBUG3);
+
+		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
+					  hbaline, tok_line->err_msg);
+	}
+
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	/* Free parse_hba_line memory */
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(hbacxt);
+}
+
+/*
+ * pg_hba_file_rules
+ *
+ * SQL-accessible set-returning function to return all the entries in the
+ * pg_hba.conf file.
+ */
+Datum
+pg_hba_file_rules(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsi;
+
+	/*
+	 * Build tuplestore to hold the result rows.  We must use the Materialize
+	 * mode to be safe against HBA file changes while the cursor is open. It's
+	 * also more efficient than having to look up our current position in the
+	 * parsed list every time.
+	 */
+	SetSingleFuncCall(fcinfo, 0);
+
+	/* Fill the tuplestore */
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+	fill_hba_view(rsi->setResult, rsi->setDesc);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 93d5190508..49688036a7 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -153,6 +153,7 @@ AttrMissing
 AttrNumber
 AttributeOpts
 AuthRequest
+AuthToken
 AutoPrewarmSharedState
 AutoVacOpts
 AutoVacuumShmemStruct
@@ -1063,7 +1064,6 @@ HashState
 HashTapeInfo
 HashValueFunc
 HbaLine
-HbaToken
 HeadlineJsonState
 HeadlineParsedText
 HeadlineWordEntry
@@ -2699,7 +2699,7 @@ ToastTupleContext
 ToastedAttribute
 TocEntry
 TokenAuxData
-TokenizedLine
+TokenizedAuthLine
 TrackItem
 TransInvalidationInfo
 TransState
-- 
2.35.1

#18Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#17)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Wed, Mar 23, 2022 at 11:03:46AM +0900, Michael Paquier wrote:

Pushing forward with 0001 by the end of the CF is the part that has no
controversy IMO, and I have no objections to it. Now, after looking
at this part, I found a few things, as of:
- HbaToken, the set of elements in the lists of TokenizedAuthLine, is
a weird to use as this layer gets used by both pg_hba.conf and
pg_indent.conf before transforming them into each HbaLine and
IdentLine. While making this part of the internals exposed, I think
that we'd better rename that to AuthToken at least. This impacts the
names of some routines internal to hba.c to copy and create
AuthTokens.

Yeah, I thought about it but didn't rename it given your concerns about git
history. I'm fine either way.

- s/gethba_options/get_hba_options/, to be consistent with
fill_hba_view() and other things.
- The comment at the top of tokenize_auth_file() needed a refresh.

That's mostly cosmetic, and the rest of the code moved is identical.
So at the end this part looks rather commitable to me.

Looks good to me, thanks.

I have not been able to test 0002 in details, but it looks rather
rather sane to me at quick glance, and it is simple. The argument
about more TAP tests applies to it, though, even if there is one SQL
test to check the function execution. It is probably better to not
consider 0003 and 0004 for this CF.

No objection to moving 0003 and 0004 to the next commitfest.

#19Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#18)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Mar 23, 2022 at 10:16:34AM +0800, Julien Rouhaud wrote:

Yeah, I thought about it but didn't rename it given your concerns about git
history. I'm fine either way.

Oh, OK. The amount of diffs created by 0001 is still fine to grab
even with the struct rename, so let's stick with it.
--
Michael

#20Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#19)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Mar 23, 2022 at 01:14:02PM +0900, Michael Paquier wrote:

On Wed, Mar 23, 2022 at 10:16:34AM +0800, Julien Rouhaud wrote:

Yeah, I thought about it but didn't rename it given your concerns about git
history. I'm fine either way.

Oh, OK. The amount of diffs created by 0001 is still fine to grab
even with the struct rename, so let's stick with it.

And so, this first part has been applied as of d4781d8. I'll try to
look at 0002 shortly.
--
Michael

#21Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#20)
Re: Allow file inclusion in pg_hba and pg_ident files

On Thu, Mar 24, 2022 at 04:50:31PM +0900, Michael Paquier wrote:

On Wed, Mar 23, 2022 at 01:14:02PM +0900, Michael Paquier wrote:

On Wed, Mar 23, 2022 at 10:16:34AM +0800, Julien Rouhaud wrote:

Yeah, I thought about it but didn't rename it given your concerns about git
history. I'm fine either way.

Oh, OK. The amount of diffs created by 0001 is still fine to grab
even with the struct rename, so let's stick with it.

And so, this first part has been applied as of d4781d8. I'll try to
look at 0002 shortly.

Thanks!

#22Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#21)
Re: Allow file inclusion in pg_hba and pg_ident files

On Thu, Mar 24, 2022 at 04:08:38PM +0800, Julien Rouhaud wrote:

On Thu, Mar 24, 2022 at 04:50:31PM +0900, Michael Paquier wrote:

And so, this first part has been applied as of d4781d8. I'll try to
look at 0002 shortly.

Thanks!

Now looking at 0002. The changes in hba.c are straight-forward,
that's a nice read.

 	if (!field) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
 						IdentFileName, line_num))); \
+		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
I think that we'd better add to err_msg the line number and the file
name.  This would become particularly important once the facility to
include files gets added.  We won't use IdentFileName for this
purpose, but at least we would know which areas to change.  Also, even
if the the view proposes line_number, there is an argument in favor of
consistency here.

+select count(*) >= 0 as ok from pg_ident_file_mappings;

I'd really like to see more tests for this stuff (pg_hba_file_rules is
not a great example in this area), where we could rely on something
for *nix platforms. Windows would provide some valid coverage as we
enable it by default for SSPI in pg_regress but that's not great for
the average Joe. One thing I thought about first is that
src/test/authentication/ does not test peer authentication at all,
which would map nicely with pg_ident.conf and some user mappings. So
we could have a new test there, that depends on how the backend reacts
when calling getpeereid(), making the results conditional. This would
be nice in the long-term.

As of a set of tests, I think that for now I would add two things, for
a total of four tests:
- Stick some queries on pg_ident_file_mappings only for Windows in
some of the tests of src/test/authentication/, say 001_password.pl.
One test should test for some valid fields. Another idea I have here
is to add some junk to pg_ident.conf, reload and check that an error
is generated (the missing field case on a given line).
- Do the same for src/test/kerberos/, with one positive and one
negative test with some junk in the ident conf file.

A last idea is to abuse of the fact that pg_ident is loaded even if we
don't use it: aka we could add some right and junky contents in
pg_ident.conf, then check its validity. Using one of the existing
tests may not be right, particularly if we finish by extending it, so
I would move that to a new fresh test script.

+    a.pg_usernamee,
[...]
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pg_username</structfield> <type>text</type>

pg_usernamee sounds a bit weird as attribute name for the view. We
could stick to a simple postgres_user_name, or postgres_name, for
example. Perhaps that's just a typo in the function output and you
intended to use pg_username?

+   /* Free parse_hba_line memory */
+   MemoryContextSwitchTo(oldcxt);
+   MemoryContextDelete(identcxt);
Incorrect comment, this should be parse_ident_line.
--
Michael
#23Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#22)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Fri, Mar 25, 2022 at 08:18:31PM +0900, Michael Paquier wrote:

Now looking at 0002. The changes in hba.c are straight-forward,
that's a nice read.

Thanks!

if (!field) { \
-		ereport(LOG, \
+		ereport(elevel, \
(errcode(ERRCODE_CONFIG_FILE_ERROR), \
errmsg("missing entry in file \"%s\" at end of line %d", \
IdentFileName, line_num))); \
+		*err_msg = psprintf("missing entry at end of line"); \
return NULL; \
} \
I think that we'd better add to err_msg the line number and the file
name.  This would become particularly important once the facility to
include files gets added.  We won't use IdentFileName for this
purpose, but at least we would know which areas to change.  Also, even
if the the view proposes line_number, there is an argument in favor of
consistency here.

I don't really like it. The file inclusion patch adds a file_name column in
both views so that you have a direct access to the information, whether the
line is in error or not. Having the file name and line number in error message
doesn't add any value as it would be redundant, and just make the view output
bigger (on top of making testing more difficult). I kept the err_msg as-is
(and fixed the ereport filename in the file inclusion patch that I indeed
missed).

+select count(*) >= 0 as ok from pg_ident_file_mappings;

I'd really like to see more tests for this stuff

I didn't like the various suggestions, as it would mean to scatter the tests
all over the place. The whole point of those views is indeed to check the
current content of a file without applying the configuration change (not on
Windows or EXEC_BACKEND, but there's nothing we can do there), so let's use
this way. I added a naive src/test/authentication/003_hba_ident_views.pl test
that validates that specific new valid and invalid lines in both files are
correctly reported. Note that I didn't update those tests for the file
inclusion.

Note that those tests fail on Windows (and I'm assuming on EXEC_BACKEND
builds), as they're testing invalid files which by definition prevent any
further connection attempt. I'm not sure what would be best to do here, apart
from bypassing the invalid config tests on such platforms. I don't think that
validating that trying to connect on such platforms when an invalid
pg_hba/pg_ident file brings anything.

+    a.pg_usernamee,
[...]
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pg_username</structfield> <type>text</type>

Perhaps that's just a typo in the function output and you
intended to use pg_username?

Yes that was a typo :) It's correctly documented in catalogs.sgml, so I just
fixed pg_proc.dat and rules.out.

+   /* Free parse_hba_line memory */
+   MemoryContextSwitchTo(oldcxt);
+   MemoryContextDelete(identcxt);
Incorrect comment, this should be parse_ident_line.

Indeed. I actually fixed it before but lost the change when rebasing after the
2nd hbafuncs.c refactoring. I also fixed an incorrect comment about
pg_hba_file_mappings.

Attachments:

v4-0001-Add-a-pg_ident_file_mappings-view.patchtext/plain; charset=us-asciiDownload
From ab685e1db37239df06ccdd7be8cfc14789e73e8c Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 17:38:34 +0800
Subject: [PATCH v4 1/3] Add a pg_ident_file_mappings view.

This view is similar to pg_hba_file_rules view, and can be also helpful to help
diagnosing configuration problems.

A following commit will add the possibility to include files in pg_hba and
pg_ident configuration files, which will then make this view even more useful.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml                    | 108 ++++++++++++++
 doc/src/sgml/client-auth.sgml                 |  10 ++
 doc/src/sgml/func.sgml                        |   5 +-
 src/backend/catalog/system_views.sql          |   6 +
 src/backend/libpq/hba.c                       |  31 ++--
 src/backend/utils/adt/hbafuncs.c              | 136 ++++++++++++++++++
 src/include/catalog/pg_proc.dat               |   7 +
 src/include/libpq/hba.h                       |   1 +
 .../authentication/t/003_hba_ident_views.pl   |  80 +++++++++++
 src/test/regress/expected/rules.out           |   6 +
 src/test/regress/expected/sysviews.out        |   6 +
 src/test/regress/sql/sysviews.sql             |   2 +
 12 files changed, 382 insertions(+), 16 deletions(-)
 create mode 100644 src/test/authentication/t/003_hba_ident_views.pl

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 94f01e4099..75fedfa07e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9591,6 +9591,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <entry>summary of client authentication configuration file contents</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link></entry>
+      <entry>summary of client user name mapping configuration file contents</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-indexes"><structname>pg_indexes</structname></link></entry>
       <entry>indexes</entry>
@@ -10589,6 +10594,109 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </para>
  </sect1>
 
+ <sect1 id="view-pg-ident-file-mappings">
+  <title><structname>pg_ident_file_mappings</structname></title>
+
+  <indexterm zone="view-pg-ident-file-mappings">
+   <primary>pg_ident_file_mappings</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_ident_file_mappings</structname> provides a summary
+   of the contents of the client user name mapping configuration file,
+   <link linkend="auth-username-maps"><filename>pg_ident.conf</filename></link>.
+   A row appears in this view for each
+   non-empty, non-comment line in the file, with annotations indicating
+   whether the rule could be applied successfully.
+  </para>
+
+  <para>
+   This view can be helpful for checking whether planned changes in the
+   authentication configuration file will work, or for diagnosing a previous
+   failure.  Note that this view reports on the <emphasis>current</emphasis>
+   contents of the file, not on what was last loaded by the server.
+  </para>
+
+  <para>
+   By default, the <structname>pg_ident_file_mappings</structname> view can be
+   read only by superusers.
+  </para>
+
+  <table>
+   <title><structname>pg_ident_file_mappings</structname> Columns</title> <tgroup
+   cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this rule in <filename>pg_ident.conf</filename>
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>map_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the map
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>sys_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Detected user name of the client
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pg_username</structfield> <type>text</type>
+      </para>
+      <para>
+       Requested PostgreSQL user name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>error</structfield> <type>text</type>
+      </para>
+      <para>
+       If not null, an error message indicating why this line could not be
+       processed
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+  <para>
+   Usually, a row reflecting an incorrect entry will have values for only
+   the <structfield>line_number</structfield> and <structfield>error</structfield> fields.
+  </para>
+
+  <para>
+   See <xref linkend="client-authentication"/> for more information about
+   client authentication configuration.
+  </para>
+ </sect1>
+
  <sect1 id="view-pg-indexes">
   <title><structname>pg_indexes</structname></title>
 
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..142b0affcb 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -896,6 +896,16 @@ mymap   /^(.*)@otherdomain\.com$   guest
    -HUP</literal>) to make it re-read the file.
   </para>
 
+  <para>
+   The system view
+   <link linkend="view-pg-ident-file-mappings"><structname>pg_ident_file_mappings</structname></link>
+   can be helpful for pre-testing changes to the
+   <filename>pg_ident.conf</filename> file, or for diagnosing problems if
+   loading of the file did not have the desired effects.  Rows in the view with
+   non-null <structfield>error</structfield> fields indicate problems in the
+   corresponding lines of the file.
+  </para>
+
   <para>
    A <filename>pg_ident.conf</filename> file that could be used in
    conjunction with the <filename>pg_hba.conf</filename> file in <xref
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8a802fb225..b32cc61886 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25475,8 +25475,9 @@ SELECT collation for ('foo' COLLATE "de_DE");
         sending a <systemitem>SIGHUP</systemitem> signal to the postmaster
         process, which in turn sends <systemitem>SIGHUP</systemitem> to each
         of its children.) You can use the
-        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link> and
-        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> views
+        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link>,
+        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> and
+        <link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link> views
         to check the configuration files for possible errors, before reloading.
        </para></entry>
       </row>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 9570a53e7b..9eaa51df29 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -617,6 +617,12 @@ CREATE VIEW pg_hba_file_rules AS
 REVOKE ALL ON pg_hba_file_rules FROM PUBLIC;
 REVOKE EXECUTE ON FUNCTION pg_hba_file_rules() FROM PUBLIC;
 
+CREATE VIEW pg_ident_file_mappings AS
+   SELECT * FROM pg_ident_file_mappings() AS A;
+
+REVOKE ALL ON pg_ident_file_mappings FROM PUBLIC;
+REVOKE EXECUTE ON FUNCTION pg_ident_file_mappings() FROM PUBLIC;
+
 CREATE VIEW pg_timezone_abbrevs AS
     SELECT * FROM pg_timezone_abbrevs();
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 673135144d..556f473b41 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -887,25 +887,22 @@ do { \
 } while (0)
 
 /*
- * Macros for handling pg_ident problems.
- * Much as above, but currently the message level is hardwired as LOG
- * and there is no provision for an err_msg string.
+ * Macros for handling pg_ident problems, similar as above.
  *
  * IDENT_FIELD_ABSENT:
- * Log a message and exit the function if the given ident field ListCell is
- * not populated.
+ * Reports when the given ident field ListCell is not populated.
  *
  * IDENT_MULTI_VALUE:
- * Log a message and exit the function if the given ident token List has more
- * than one element.
+ * Reports when the given ident token List has more than one element.
  */
 #define IDENT_FIELD_ABSENT(field) \
 do { \
 	if (!field) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
 						IdentFileName, line_num))); \
+		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
 } while (0)
@@ -913,11 +910,12 @@ do { \
 #define IDENT_MULTI_VALUE(tokens) \
 do { \
 	if (tokens->length > 1) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
 							line_num, IdentFileName))); \
+		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
 } while (0)
@@ -2306,7 +2304,8 @@ load_hba(void)
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
  *
- * If parsing fails, log a message and return NULL.
+ * If parsing fails, log a message at ereport level elevel, store an error
+ * string in tok_line->err_msg and return NULL.
  *
  * If ident_user is a regular expression (ie. begins with a slash), it is
  * compiled and stored in IdentLine structure.
@@ -2315,10 +2314,11 @@ load_hba(void)
  * to have set a memory context that will be reset if this function returns
  * NULL.
  */
-static IdentLine *
-parse_ident_line(TokenizedAuthLine *tok_line)
+IdentLine *
+parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
 	List	   *tokens;
 	AuthToken  *token;
@@ -2372,11 +2372,14 @@ parse_ident_line(TokenizedAuthLine *tok_line)
 			char		errstr[100];
 
 			pg_regerror(r, &parsedline->re, errstr, sizeof(errstr));
-			ereport(LOG,
+			ereport(elevel,
 					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
 					 errmsg("invalid regular expression \"%s\": %s",
 							parsedline->ident_user + 1, errstr)));
 
+			*err_msg = psprintf("invalid regular expression \"%s\": %s",
+							   parsedline->ident_user + 1, errstr);
+
 			pfree(wstr);
 			return NULL;
 		}
@@ -2627,7 +2630,7 @@ load_ident(void)
 			continue;
 		}
 
-		if ((newline = parse_ident_line(tok_line)) == NULL)
+		if ((newline = parse_ident_line(tok_line, LOG)) == NULL)
 		{
 			/* Parse error; remember there's trouble */
 			ok = false;
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index f46cd935a1..1970b4c497 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -28,6 +28,9 @@ static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 						  int lineno, HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int lineno, IdentLine *ident, const char *err_msg);
+static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
 /*
@@ -426,3 +429,136 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 
 	PG_RETURN_NULL();
 }
+
+/* Number of columns in pg_ident_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+
+/*
+ * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
+ * tuplestore
+ *
+ * tuple_store: where to store data
+ * tupdesc: tuple descriptor for the view
+ * lineno: pg_hba.conf line number (must always be valid)
+ * ident: parsed line data (can be NULL, in which case err_msg should be set)
+ * err_msg: error message (NULL if none)
+ *
+ * Note: leaks memory, but we don't care since this is run in a short-lived
+ * memory context.
+ */
+static void
+fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+				int lineno, IdentLine *ident, const char *err_msg)
+{
+	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	HeapTuple	tuple;
+	int			index;
+
+	Assert(tupdesc->natts == NUM_PG_IDENT_FILE_MAPPINGS_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (ident != NULL)
+	{
+		values[index++] = CStringGetTextDatum(ident->usermap);
+		values[index++] = CStringGetTextDatum(ident->ident_user);
+		values[index++] = CStringGetTextDatum(ident->pg_role);
+	}
+	else
+	{
+		/* no parsing result, so set relevant fields to nulls */
+		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * Read the pg_ident.conf file and fill the tuplestore with view records.
+ */
+static void
+fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *ident_lines = NIL;
+	ListCell   *line;
+	MemoryContext linecxt;
+	MemoryContext identcxt;
+	MemoryContext oldcxt;
+
+	/*
+	 * In the unlikely event that we can't open pg_hba.conf, we throw an
+	 * error, rather than trying to report it via some sort of view entry.
+	 * (Most other error conditions should result in a message in a view
+	 * entry.)
+	 */
+	file = AllocateFile(IdentFileName, "r");
+	if (file == NULL)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open usermap file \"%s\": %m",
+						IdentFileName)));
+
+	linecxt = tokenize_auth_file(HbaFileName, file, &ident_lines, DEBUG3);
+	FreeFile(file);
+
+	/* Now parse all the lines */
+	identcxt = AllocSetContextCreate(CurrentMemoryContext,
+								   "ident parser context",
+								   ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(identcxt);
+	foreach(line, ident_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		IdentLine   *identline = NULL;
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg == NULL)
+			identline = parse_ident_line(tok_line, DEBUG3);
+
+		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
+					  tok_line->err_msg);
+	}
+
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	/* Free parse_ident_line memory */
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(identcxt);
+}
+
+/*
+ * SQL-accessible SRF to return all the entries in the pg_ident.conf file.
+ */
+Datum
+pg_ident_file_mappings(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsi;
+
+	/*
+	 * Build tuplestore to hold the result rows.  We must use the Materialize
+	 * mode to be safe against HBA file changes while the cursor is open.
+	 * It's also more efficient than having to look up our current position in
+	 * the parsed list every time.
+	 */
+	SetSingleFuncCall(fcinfo, 0);
+
+	/* Fill the tuplestore */
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+	fill_ident_view(rsi->setResult, rsi->setDesc);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 699bd0aa3e..abc9a83223 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6115,6 +6115,13 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o}',
   proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
+{ oid => '9556', descr => 'show pg_ident.conf mappings',
+  proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
+  provolatile => 'v', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o}',
+  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 13ecb329f8..90036f7bcd 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -171,6 +171,7 @@ extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
 						  bool case_sensitive);
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
+extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
 										List **tok_lines, int elevel);
diff --git a/src/test/authentication/t/003_hba_ident_views.pl b/src/test/authentication/t/003_hba_ident_views.pl
new file mode 100644
index 0000000000..86bb9d9b27
--- /dev/null
+++ b/src/test/authentication/t/003_hba_ident_views.pl
@@ -0,0 +1,80 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for checkig pg_hba_file_rules and pg_ident_file_mappings views.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Delete pg_hba.conf from the given node, add a new entry to it
+# and then execute a reload to refresh it.
+sub reset_pg_hba
+{
+	my $node       = shift;
+	my $hba_method = shift;
+
+	unlink($node->data_dir . '/pg_hba.conf');
+	# just for testing purposes, use a continuation line
+	$node->append_conf('pg_hba.conf', "local all all\\\n $hba_method");
+	$node->reload;
+	return;
+}
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $result;
+
+# Check that the initial views don't report any error
+$result = $node->safe_psql('postgres',
+	"SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL");
+is($result, '0', 'There should not be error in pg_hba_file_rules');
+$result = $node->safe_psql('postgres',
+	"SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL");
+is($result, '0', 'There should not be error in pg_ident_file_mappings');
+
+# Add some sample lines in pg_hba/pg_ident conf files
+$node->append_conf('pg_hba.conf', "host somedb someuser 1.2.3.4/32 reject");
+$node->append_conf('pg_hba.conf', "host somedb someuser zuul");
+$node->append_conf('pg_ident.conf', "somemap someosuser somepgrole");
+$node->append_conf('pg_ident.conf', "zuul");
+
+# Check pg_hba newly added valid line is found
+$result = $node->safe_psql('postgres',
+	qq(SELECT type, database, user_name, address, netmask, auth_method, options
+		FROM pg_hba_file_rules
+		WHERE error IS NULL
+		ORDER BY line_number DESC LIMIT 1;
+	));
+is($result, 'host|{somedb}|{someuser}|1.2.3.4|255.255.255.255|reject|');
+
+# Check pg_hba newly added incorrect line
+$result = $node->safe_psql('postgres',
+	qq(SELECT error
+		FROM pg_hba_file_rules
+		WHERE error IS NOT NULL;
+	));
+is($result, 'end-of-line before authentication method');
+
+# Check pg_ident newly added valid line is found
+$result = $node->safe_psql('postgres',
+	qq(SELECT map_name, sys_name, pg_username
+		FROM pg_ident_file_mappings
+		WHERE error IS NULL;
+	));
+is($result, 'somemap|someosuser|somepgrole');
+
+# Check pg_hba newly added incorrect line
+$result = $node->safe_psql('postgres',
+	qq(SELECT error
+		FROM pg_ident_file_mappings
+		WHERE error IS NOT NULL;
+	));
+is($result, 'missing entry at end of line');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 27d19b4bf1..5a20e5a7e1 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1347,6 +1347,12 @@ pg_hba_file_rules| SELECT a.line_number,
     a.options,
     a.error
    FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.line_number,
+    a.map_name,
+    a.sys_name,
+    a.pg_username,
+    a.error
+   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 442eeb1e3f..31ba549883 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -55,6 +55,12 @@ select count(*) > 0 as ok from pg_hba_file_rules;
  t
 (1 row)
 
+select count(*) >= 0 as ok from pg_ident_file_mappings;
+ ok 
+----
+ t
+(1 row)
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
  ok 
diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql
index 4980f07be2..1148014e47 100644
--- a/src/test/regress/sql/sysviews.sql
+++ b/src/test/regress/sql/sysviews.sql
@@ -28,6 +28,8 @@ select count(*) >= 0 as ok from pg_file_settings;
 -- There will surely be at least one rule
 select count(*) > 0 as ok from pg_hba_file_rules;
 
+select count(*) >= 0 as ok from pg_ident_file_mappings;
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
 
-- 
2.33.1

v4-0002-Allow-file-inclusion-in-pg_hba-and-pg_ident-files.patchtext/plain; charset=us-asciiDownload
From 9fff95d2c86f2e777401561623e922ec4ecbc245 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 15:45:26 +0800
Subject: [PATCH v4 2/3] Allow file inclusion in pg_hba and pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml             |  48 +++++-
 doc/src/sgml/client-auth.sgml          |  34 +++-
 src/backend/libpq/hba.c                | 229 +++++++++++++++++++------
 src/backend/libpq/pg_hba.conf.sample   |   8 +-
 src/backend/libpq/pg_ident.conf.sample |   8 +-
 src/backend/utils/adt/hbafuncs.c       |  53 ++++--
 src/include/catalog/pg_proc.dat        |  12 +-
 src/include/libpq/hba.h                |   2 +
 src/test/regress/expected/rules.out    |  12 +-
 9 files changed, 321 insertions(+), 85 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 75fedfa07e..bd1c9a8d21 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -10496,12 +10496,31 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule in the given file_name
       </para></entry>
      </row>
 
@@ -10637,6 +10656,33 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this mapping if the mapping is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this mapping
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this mapping in the given file_name
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 142b0affcb..e1d0e103b3 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,17 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,6 +112,7 @@
   <para>
    A record can have several formats:
 <synopsis>
+include       <replaceable>file</replaceable>
 local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
 host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
@@ -118,6 +128,15 @@ hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceabl
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -835,8 +854,9 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -847,6 +867,14 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   The lines can record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 556f473b41..1551b34c53 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -68,6 +68,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -112,10 +118,16 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
 
 
 /*
@@ -355,36 +367,11 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, outer_filename,
+							 elevel, err_msg, &inc_fullname);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
 	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
@@ -425,11 +412,36 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a decicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -438,30 +450,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -522,29 +526,76 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+				char	   *inc_fullname;
+				FILE	   *inc_file;
+
+				inc_filename = second->string;
+
+				inc_file = open_inc_file(IncludedAuthFile, inc_filename,
+										 filename, elevel, &err_msg,
+										 &inc_fullname);
+
+				/*
+				 * The included file could be open, now recursively process it.
+				 * Errors will be reported in the general TokenizedAuthLine
+				 * processing.
+				 */
+				if (inc_file != NULL)
+				{
+					tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+											   tok_lines, elevel);
+
+					FreeFile(inc_file);
+					pfree(inc_fullname);
+
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+				else
+				{
+					/* We should got an error */
+					Assert(err_msg != NULL);
+				}
+			}
 		}
 
+		/* General processing: emit line to the TokenizedAuthLine */
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
+
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -859,7 +910,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -879,7 +930,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -901,7 +952,7 @@ do { \
 		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
-						IdentFileName, line_num))); \
+						tok_line->file_name, line_num))); \
 		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -914,7 +965,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, tok_line->file_name))); \
 		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -937,6 +988,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -951,6 +1003,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -1677,6 +1730,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -2299,6 +2353,67 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		switch (kind)
+		{
+			case SecondaryAuthFile:
+				msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+				msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+				break;
+			case IncludedAuthFile:
+				msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+				msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+				break;
+			default:
+				elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+				break;
+		}
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg(msglog, inc_filename, *inc_fullname)));
+		*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+							strerror(save_errno));
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..0b6589a7b9 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,6 +9,7 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
+# include       FILE
 # local         DATABASE  USER  METHOD  [OPTIONS]
 # host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 # hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
@@ -18,7 +19,12 @@
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", it's not a mapping record but a directive to
+# include records from another file, specified in the field.  FILE is the file
+# to include.  It can be specified with a relative or absolute path, and can be
+# double quoted if it contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..138359cf03 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,18 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
+# include  FILE
 # MAPNAME  SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", it's not an authentication record but a
+# directive to include records from another file, specified in the field.  FILE
+# is the file to include.  It can be specified with a relative or absolute
+# path, and can be double quoted if it contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 1970b4c497..33cf5fb954 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,9 +26,11 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int mapping_number, const char *filename,
 							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -174,7 +176,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +196,13 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +346,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +369,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,8 +404,12 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -430,8 +445,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,13 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +496,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +518,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,8 +553,13 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-					  tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index abc9a83223..87842d5ce2 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6111,16 +6111,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '9556', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 90036f7bcd..59f6faf9f8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -155,6 +156,7 @@ typedef struct AuthToken
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 5a20e5a7e1..e2c6aa8ffb 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,9 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1348,15 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.33.1

v4-0003-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From 6b9642a2f5d202ce04c73d2ac69df7be03624ee9 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v4 3/3] POC: Add a pg_hba_matches() function.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 81bac6f581..049cdabc81 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 1551b34c53..a757a54c87 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -26,6 +26,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -41,6 +42,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2835,3 +2837,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 87842d5ce2..3a9c1261dc 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6122,6 +6122,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.33.1

#24Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#23)
Re: Allow file inclusion in pg_hba and pg_ident files

On Sun, Mar 27, 2022 at 05:52:22PM +0800, Julien Rouhaud wrote:

I didn't like the various suggestions, as it would mean to scatter the tests
all over the place. The whole point of those views is indeed to check the
current content of a file without applying the configuration change (not on
Windows or EXEC_BACKEND, but there's nothing we can do there), so let's use
this way. I added a naive src/test/authentication/003_hba_ident_views.pl test
that validates that specific new valid and invalid lines in both files are
correctly reported. Note that I didn't update those tests for the file
inclusion.

I see.  The tests ought to be more extensive though, as hba.c checks
for multiple or missing fields for a varying number of expecting
parameters.  Here are the patterns that would cover most of the
failures of the backend.  For hba.conf:
+host
+host incorrect_db
+host incorrect_db incorrect_user
+host incorrect_db incorrect_user incorrect_host
For pg_ident.conf:
+# Error with incomplete lines.
+incorrect_map
+incorrect_map os_user
+# Errors with lines that have multiple values, for each field.
+incorrect_map_1,incorrect_map_2
+incorrect_map os_user_1,os_user_2
+incorrect_map os_user pg_role_1,pg_role_2

Then I was thinking about doing full scan of each view and could the
expected errors with some GROUP BY magic. See the attached, for
reference, but it would fail with EXEC_BACKEND on WIN32.

Note that those tests fail on Windows (and I'm assuming on EXEC_BACKEND
builds), as they're testing invalid files which by definition prevent any
further connection attempt. I'm not sure what would be best to do here, apart
from bypassing the invalid config tests on such platforms. I don't think that
validating that trying to connect on such platforms when an invalid
pg_hba/pg_ident file brings anything.

Hmm, indeed. I have been looking at that and that's annoying. As you
mentioned off-list, in order to know if a build has an emulation of
fork() that would avoid failures with new connection attempts after
including some dummy entries in pg_hba.conf or pg_ident.conf, we'd
need to look at EXEC_BACKEND (well, mostly), as of:
- CFLAGS in pg_config, or a query on pg_config() (does not work with
MSVC as CFLAGS is not set there).
- A potential check on pg_config{_manual}.h.
- Perhaps an extra dependency on $windows_os, discarding incorrectly
cygwin that should be able to work.

We could use a failure path for each psql command rather than a SKIP
block, as you told me, if the psql fails and check that we get some
error strings related to the loading of auth files. However, I am
scared of this design in the long-term as it could cause the tests to
pass with a failure triggered on platforms and/or configurations where
we should have a success. So, I am tempted to drop the ball for now
with the TAP part.

The patch still has value for the end-user. I have checked the
backend part, and I did not notice any obvious issue. There is one
thing that I am wondering though: should we change the two queries in
sysviews.sql so as we check that there are zero errors in the two
views when the files are parsed? This simple change would avoid
mistakes for users running installcheck on a production installation.
--
Michael

#25Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#24)
1 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Mar 28, 2022 at 04:20:07PM +0900, Michael Paquier wrote:

See the attached, for reference, but it would fail with EXEC_BACKEND
on WIN32.

Ditto.
--
Michael

Attachments:

v5-0001-Add-a-pg_ident_file_mappings-view.patchtext/x-diff; charset=us-asciiDownload
From 69e02734fd0199ba02cc34bc468b04584bdf0efd Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 28 Mar 2022 16:20:40 +0900
Subject: [PATCH v5] Add a pg_ident_file_mappings view.

This view is similar to pg_hba_file_rules view, and can be also helpful to help
diagnosing configuration problems.

A following commit will add the possibility to include files in pg_hba and
pg_ident configuration files, which will then make this view even more useful.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat             |   6 +
 src/include/libpq/hba.h                     |   1 +
 src/backend/catalog/system_views.sql        |   6 +
 src/backend/libpq/hba.c                     |  31 +++--
 src/backend/utils/adt/hbafuncs.c            | 136 ++++++++++++++++++++
 src/test/authentication/t/003_auth_views.pl | 108 ++++++++++++++++
 src/test/regress/expected/rules.out         |   6 +
 src/test/regress/expected/sysviews.out      |   6 +
 src/test/regress/sql/sysviews.sql           |   2 +
 doc/src/sgml/catalogs.sgml                  | 108 ++++++++++++++++
 doc/src/sgml/client-auth.sgml               |  10 ++
 doc/src/sgml/func.sgml                      |   5 +-
 12 files changed, 409 insertions(+), 16 deletions(-)
 create mode 100644 src/test/authentication/t/003_auth_views.pl

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5e612a6b67..915bc19176 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6115,6 +6115,12 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o}',
   proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
+{ oid => '9556', descr => 'show pg_ident.conf mappings',
+  proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
+  provolatile => 'v', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
+  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 13ecb329f8..90036f7bcd 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -171,6 +171,7 @@ extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
 						  bool case_sensitive);
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
+extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
 										List **tok_lines, int elevel);
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 9570a53e7b..9eaa51df29 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -617,6 +617,12 @@ CREATE VIEW pg_hba_file_rules AS
 REVOKE ALL ON pg_hba_file_rules FROM PUBLIC;
 REVOKE EXECUTE ON FUNCTION pg_hba_file_rules() FROM PUBLIC;
 
+CREATE VIEW pg_ident_file_mappings AS
+   SELECT * FROM pg_ident_file_mappings() AS A;
+
+REVOKE ALL ON pg_ident_file_mappings FROM PUBLIC;
+REVOKE EXECUTE ON FUNCTION pg_ident_file_mappings() FROM PUBLIC;
+
 CREATE VIEW pg_timezone_abbrevs AS
     SELECT * FROM pg_timezone_abbrevs();
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 673135144d..f8393ca8ed 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -887,25 +887,22 @@ do { \
 } while (0)
 
 /*
- * Macros for handling pg_ident problems.
- * Much as above, but currently the message level is hardwired as LOG
- * and there is no provision for an err_msg string.
+ * Macros for handling pg_ident problems, similar as above.
  *
  * IDENT_FIELD_ABSENT:
- * Log a message and exit the function if the given ident field ListCell is
- * not populated.
+ * Reports when the given ident field ListCell is not populated.
  *
  * IDENT_MULTI_VALUE:
- * Log a message and exit the function if the given ident token List has more
- * than one element.
+ * Reports when the given ident token List has more than one element.
  */
 #define IDENT_FIELD_ABSENT(field) \
 do { \
 	if (!field) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
 						IdentFileName, line_num))); \
+		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
 } while (0)
@@ -913,11 +910,12 @@ do { \
 #define IDENT_MULTI_VALUE(tokens) \
 do { \
 	if (tokens->length > 1) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
 							line_num, IdentFileName))); \
+		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
 } while (0)
@@ -2306,7 +2304,8 @@ load_hba(void)
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
  *
- * If parsing fails, log a message and return NULL.
+ * If parsing fails, log a message at ereport level elevel, store an error
+ * string in tok_line->err_msg and return NULL.
  *
  * If ident_user is a regular expression (ie. begins with a slash), it is
  * compiled and stored in IdentLine structure.
@@ -2315,10 +2314,11 @@ load_hba(void)
  * to have set a memory context that will be reset if this function returns
  * NULL.
  */
-static IdentLine *
-parse_ident_line(TokenizedAuthLine *tok_line)
+IdentLine *
+parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
 	List	   *tokens;
 	AuthToken  *token;
@@ -2372,11 +2372,14 @@ parse_ident_line(TokenizedAuthLine *tok_line)
 			char		errstr[100];
 
 			pg_regerror(r, &parsedline->re, errstr, sizeof(errstr));
-			ereport(LOG,
+			ereport(elevel,
 					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
 					 errmsg("invalid regular expression \"%s\": %s",
 							parsedline->ident_user + 1, errstr)));
 
+			*err_msg = psprintf("invalid regular expression \"%s\": %s",
+								parsedline->ident_user + 1, errstr);
+
 			pfree(wstr);
 			return NULL;
 		}
@@ -2627,7 +2630,7 @@ load_ident(void)
 			continue;
 		}
 
-		if ((newline = parse_ident_line(tok_line)) == NULL)
+		if ((newline = parse_ident_line(tok_line, LOG)) == NULL)
 		{
 			/* Parse error; remember there's trouble */
 			ok = false;
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index f46cd935a1..ee70c7115c 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -28,6 +28,9 @@ static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 						  int lineno, HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int lineno, IdentLine *ident, const char *err_msg);
+static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
 /*
@@ -426,3 +429,136 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 
 	PG_RETURN_NULL();
 }
+
+/* Number of columns in pg_ident_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+
+/*
+ * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
+ * tuplestore
+ *
+ * tuple_store: where to store data
+ * tupdesc: tuple descriptor for the view
+ * lineno: pg_hba.conf line number (must always be valid)
+ * ident: parsed line data (can be NULL, in which case err_msg should be set)
+ * err_msg: error message (NULL if none)
+ *
+ * Note: leaks memory, but we don't care since this is run in a short-lived
+ * memory context.
+ */
+static void
+fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+				int lineno, IdentLine *ident, const char *err_msg)
+{
+	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	HeapTuple	tuple;
+	int			index;
+
+	Assert(tupdesc->natts == NUM_PG_IDENT_FILE_MAPPINGS_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (ident != NULL)
+	{
+		values[index++] = CStringGetTextDatum(ident->usermap);
+		values[index++] = CStringGetTextDatum(ident->ident_user);
+		values[index++] = CStringGetTextDatum(ident->pg_role);
+	}
+	else
+	{
+		/* no parsing result, so set relevant fields to nulls */
+		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * Read the pg_ident.conf file and fill the tuplestore with view records.
+ */
+static void
+fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *ident_lines = NIL;
+	ListCell   *line;
+	MemoryContext linecxt;
+	MemoryContext identcxt;
+	MemoryContext oldcxt;
+
+	/*
+	 * In the unlikely event that we can't open pg_hba.conf, we throw an
+	 * error, rather than trying to report it via some sort of view entry.
+	 * (Most other error conditions should result in a message in a view
+	 * entry.)
+	 */
+	file = AllocateFile(IdentFileName, "r");
+	if (file == NULL)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open usermap file \"%s\": %m",
+						IdentFileName)));
+
+	linecxt = tokenize_auth_file(HbaFileName, file, &ident_lines, DEBUG3);
+	FreeFile(file);
+
+	/* Now parse all the lines */
+	identcxt = AllocSetContextCreate(CurrentMemoryContext,
+									 "ident parser context",
+									 ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(identcxt);
+	foreach(line, ident_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		IdentLine  *identline = NULL;
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg == NULL)
+			identline = parse_ident_line(tok_line, DEBUG3);
+
+		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
+						tok_line->err_msg);
+	}
+
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	/* Free parse_ident_line memory */
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(identcxt);
+}
+
+/*
+ * SQL-accessible SRF to return all the entries in the pg_ident.conf file.
+ */
+Datum
+pg_ident_file_mappings(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsi;
+
+	/*
+	 * Build tuplestore to hold the result rows.  We must use the Materialize
+	 * mode to be safe against HBA file changes while the cursor is open. It's
+	 * also more efficient than having to look up our current position in the
+	 * parsed list every time.
+	 */
+	SetSingleFuncCall(fcinfo, 0);
+
+	/* Fill the tuplestore */
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+	fill_ident_view(rsi->setResult, rsi->setDesc);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/test/authentication/t/003_auth_views.pl b/src/test/authentication/t/003_auth_views.pl
new file mode 100644
index 0000000000..c237a50788
--- /dev/null
+++ b/src/test/authentication/t/003_auth_views.pl
@@ -0,0 +1,108 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests checking pg_hba_file_rules and pg_ident_file_mappings.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $result;
+
+# Check that the initial views don't report any error
+$result = $node->safe_psql('postgres',
+	"SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL");
+is($result, '0', 'no errors in the initial setup of pg_hba.conf');
+$result = $node->safe_psql('postgres',
+	"SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL");
+is($result, '0', 'no errors in the initial setup of pg_ident.conf');
+
+# Builds with -DEXEC_BACKEND would attempt to use the updated configuration
+# files for each new connection.
+my $exec_backend = $node->safe_psql('postgres',
+	qq(SELECT count(name) FROM pg_config WHERE name = 'CFLAGS' AND setting ~ 'EXEC_BACKEND';)
+);
+
+SKIP:
+{
+	skip "cancel test requires a Unix shell", 4 if $exec_backend;
+
+	# Add some sample lines in pg_hba/pg_ident conf files to trigger more
+	# behaviors.  No reload is needed, as each system view tested above does
+	# its own parsing of the configuration files it works on when executed.
+	$node->append_conf(
+		'pg_hba.conf', qq(
+# Correct line, parsed correctly.
+host dummy_db dummy_user 1.2.3.4/32 reject
+# Incomplete line, leading to an error
+host
+host incorrect_db
+host incorrect_db incorrect_user
+host incorrect_db incorrect_user incorrect_host
+));
+	$node->append_conf(
+		'pg_ident.conf', qq(
+# Correct line, parsed correctly.
+dummy_map dummy_os_user dummy_pg_role
+# Error with incomplete lines.
+incorrect_map
+incorrect_map os_user
+# Errors with lines that have multiple values, for each field.
+incorrect_map_1,incorrect_map_2
+incorrect_map os_user_1,os_user_2
+incorrect_map os_user pg_role_1,pg_role_2
+));
+
+	# Check the existence of the dummy, still correct, entry added above.
+	$result = $node->safe_psql(
+		'postgres',
+		qq(SELECT type, database, address, netmask, auth_method
+		FROM pg_hba_file_rules
+		WHERE error IS NULL AND user_name[1] = 'dummy_user'
+		ORDER BY line_number DESC LIMIT 1;
+	));
+	is( $result,
+		qq(host|{dummy_db}|1.2.3.4|255.255.255.255|reject),
+		'parsed dummy line of pg_hba.conf without errors');
+
+	# Check pg_hba.conf for its expected set of errors.
+	$result = $node->safe_psql(
+		'postgres',
+		qq(SELECT error, count(error) FROM pg_hba_file_rules
+WHERE error IS NOT NULL GROUP BY error ORDER BY error;));
+	is( $result, qq(end-of-line before authentication method|1
+end-of-line before database specification|1
+end-of-line before IP address specification|1
+end-of-line before role specification|1),
+		'parsed dummy lines of pg_hba.conf with expected errors');
+
+	# Check the existence of the dummy, still correct, entry added above.
+	$result = $node->safe_psql(
+		'postgres',
+		qq(SELECT sys_name, pg_username
+		FROM pg_ident_file_mappings WHERE map_name = 'dummy_map'
+		AND error IS NULL;
+	));
+	is( $result,
+		qq(dummy_os_user|dummy_pg_role),
+		'parsed dummy line of pg_ident.conf without errors');
+
+	# Check pg_ident.conf for its expected set of errors.
+	$result = $node->safe_psql(
+		'postgres',
+		qq(SELECT error, count(error) FROM pg_ident_file_mappings
+WHERE error IS NOT NULL GROUP BY error ORDER BY error;));
+	is( $result, qq(missing entry at end of line|2
+multiple values in ident field|3),
+		'parsed dummy lines of pg_ident.conf with expected errors');
+
+}
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 27d19b4bf1..5a20e5a7e1 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1347,6 +1347,12 @@ pg_hba_file_rules| SELECT a.line_number,
     a.options,
     a.error
    FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.line_number,
+    a.map_name,
+    a.sys_name,
+    a.pg_username,
+    a.error
+   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 442eeb1e3f..31ba549883 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -55,6 +55,12 @@ select count(*) > 0 as ok from pg_hba_file_rules;
  t
 (1 row)
 
+select count(*) >= 0 as ok from pg_ident_file_mappings;
+ ok 
+----
+ t
+(1 row)
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
  ok 
diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql
index 4980f07be2..1148014e47 100644
--- a/src/test/regress/sql/sysviews.sql
+++ b/src/test/regress/sql/sysviews.sql
@@ -28,6 +28,8 @@ select count(*) >= 0 as ok from pg_file_settings;
 -- There will surely be at least one rule
 select count(*) > 0 as ok from pg_hba_file_rules;
 
+select count(*) >= 0 as ok from pg_ident_file_mappings;
+
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
 
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 94f01e4099..75fedfa07e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9591,6 +9591,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <entry>summary of client authentication configuration file contents</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link></entry>
+      <entry>summary of client user name mapping configuration file contents</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-indexes"><structname>pg_indexes</structname></link></entry>
       <entry>indexes</entry>
@@ -10589,6 +10594,109 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </para>
  </sect1>
 
+ <sect1 id="view-pg-ident-file-mappings">
+  <title><structname>pg_ident_file_mappings</structname></title>
+
+  <indexterm zone="view-pg-ident-file-mappings">
+   <primary>pg_ident_file_mappings</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_ident_file_mappings</structname> provides a summary
+   of the contents of the client user name mapping configuration file,
+   <link linkend="auth-username-maps"><filename>pg_ident.conf</filename></link>.
+   A row appears in this view for each
+   non-empty, non-comment line in the file, with annotations indicating
+   whether the rule could be applied successfully.
+  </para>
+
+  <para>
+   This view can be helpful for checking whether planned changes in the
+   authentication configuration file will work, or for diagnosing a previous
+   failure.  Note that this view reports on the <emphasis>current</emphasis>
+   contents of the file, not on what was last loaded by the server.
+  </para>
+
+  <para>
+   By default, the <structname>pg_ident_file_mappings</structname> view can be
+   read only by superusers.
+  </para>
+
+  <table>
+   <title><structname>pg_ident_file_mappings</structname> Columns</title> <tgroup
+   cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this rule in <filename>pg_ident.conf</filename>
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>map_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the map
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>sys_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Detected user name of the client
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pg_username</structfield> <type>text</type>
+      </para>
+      <para>
+       Requested PostgreSQL user name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>error</structfield> <type>text</type>
+      </para>
+      <para>
+       If not null, an error message indicating why this line could not be
+       processed
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+  <para>
+   Usually, a row reflecting an incorrect entry will have values for only
+   the <structfield>line_number</structfield> and <structfield>error</structfield> fields.
+  </para>
+
+  <para>
+   See <xref linkend="client-authentication"/> for more information about
+   client authentication configuration.
+  </para>
+ </sect1>
+
  <sect1 id="view-pg-indexes">
   <title><structname>pg_indexes</structname></title>
 
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..142b0affcb 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -896,6 +896,16 @@ mymap   /^(.*)@otherdomain\.com$   guest
    -HUP</literal>) to make it re-read the file.
   </para>
 
+  <para>
+   The system view
+   <link linkend="view-pg-ident-file-mappings"><structname>pg_ident_file_mappings</structname></link>
+   can be helpful for pre-testing changes to the
+   <filename>pg_ident.conf</filename> file, or for diagnosing problems if
+   loading of the file did not have the desired effects.  Rows in the view with
+   non-null <structfield>error</structfield> fields indicate problems in the
+   corresponding lines of the file.
+  </para>
+
   <para>
    A <filename>pg_ident.conf</filename> file that could be used in
    conjunction with the <filename>pg_hba.conf</filename> file in <xref
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8a802fb225..b32cc61886 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25475,8 +25475,9 @@ SELECT collation for ('foo' COLLATE "de_DE");
         sending a <systemitem>SIGHUP</systemitem> signal to the postmaster
         process, which in turn sends <systemitem>SIGHUP</systemitem> to each
         of its children.) You can use the
-        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link> and
-        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> views
+        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link>,
+        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> and
+        <link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link> views
         to check the configuration files for possible errors, before reloading.
        </para></entry>
       </row>
-- 
2.35.1

#26Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#24)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Mon, Mar 28, 2022 at 04:20:07PM +0900, Michael Paquier wrote:

Note that those tests fail on Windows (and I'm assuming on EXEC_BACKEND
builds), as they're testing invalid files which by definition prevent any
further connection attempt. I'm not sure what would be best to do here, apart
from bypassing the invalid config tests on such platforms. I don't think that
validating that trying to connect on such platforms when an invalid
pg_hba/pg_ident file brings anything.

Hmm, indeed. I have been looking at that and that's annoying. As you
mentioned off-list, in order to know if a build has an emulation of
fork() that would avoid failures with new connection attempts after
including some dummy entries in pg_hba.conf or pg_ident.conf, we'd
need to look at EXEC_BACKEND (well, mostly), as of:
- CFLAGS in pg_config, or a query on pg_config() (does not work with
MSVC as CFLAGS is not set there).
- A potential check on pg_config{_manual}.h.
- Perhaps an extra dependency on $windows_os, discarding incorrectly
cygwin that should be able to work.

Yeah, detecting !cygwin-Windows or EXEC_BACKEND in the TAP tests is quite hard.

We could use a failure path for each psql command rather than a SKIP
block, as you told me, if the psql fails and check that we get some
error strings related to the loading of auth files. However, I am
scared of this design in the long-term as it could cause the tests to
pass with a failure triggered on platforms and/or configurations where
we should have a success. So, I am tempted to drop the ball for now
with the TAP part.

Ok. We could still keep the tests for the valid lines part though?

The patch still has value for the end-user. I have checked the
backend part, and I did not notice any obvious issue. There is one
thing that I am wondering though: should we change the two queries in
sysviews.sql so as we check that there are zero errors in the two
views when the files are parsed? This simple change would avoid
mistakes for users running installcheck on a production installation.

Do you mean something like

SELECT count(*) > 0 AS ok,
count(*) FILTER (WHERE error IS NOT NULL) = 0 AS has_no_error
FROM pg_hba_file_rules ;

and similar for pg_ident_rule_mappings?

#27Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#26)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Mar 28, 2022 at 03:43:41PM +0800, Julien Rouhaud wrote:

On Mon, Mar 28, 2022 at 04:20:07PM +0900, Michael Paquier wrote:

We could use a failure path for each psql command rather than a SKIP
block, as you told me, if the psql fails and check that we get some
error strings related to the loading of auth files. However, I am
scared of this design in the long-term as it could cause the tests to
pass with a failure triggered on platforms and/or configurations where
we should have a success. So, I am tempted to drop the ball for now
with the TAP part.

Ok. We could still keep the tests for the valid lines part though?

With the SQLs modified as below, this part is less interesting.

The patch still has value for the end-user. I have checked the
backend part, and I did not notice any obvious issue. There is one
thing that I am wondering though: should we change the two queries in
sysviews.sql so as we check that there are zero errors in the two
views when the files are parsed? This simple change would avoid
mistakes for users running installcheck on a production installation.

Do you mean something like

SELECT count(*) > 0 AS ok,
count(*) FILTER (WHERE error IS NOT NULL) = 0 AS has_no_error
FROM pg_hba_file_rules ;

and similar for pg_ident_rule_mappings?

Something like that, yes.
--
Michael

#28Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#27)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Mar 28, 2022 at 05:02:14PM +0900, Michael Paquier wrote:

On Mon, Mar 28, 2022 at 03:43:41PM +0800, Julien Rouhaud wrote:

Ok. We could still keep the tests for the valid lines part though?

With the SQLs modified as below, this part is less interesting.

Ok.

Do you mean something like

SELECT count(*) > 0 AS ok,
count(*) FILTER (WHERE error IS NOT NULL) = 0 AS has_no_error
FROM pg_hba_file_rules ;

and similar for pg_ident_rule_mappings?

Something like that, yes.

Ok, v5 attached without the TAP tests and updated sysviews tests.

Attachments:

v5-0001-Add-a-pg_ident_file_mappings-view.patchtext/plain; charset=us-asciiDownload
From 681c16db9fcb4e27ebffffba28e6a0f2134de071 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 17:38:34 +0800
Subject: [PATCH v5 1/3] Add a pg_ident_file_mappings view.

This view is similar to pg_hba_file_rules view, and can be also helpful to help
diagnosing configuration problems.

A following commit will add the possibility to include files in pg_hba and
pg_ident configuration files, which will then make this view even more useful.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml             | 108 ++++++++++++++++++++
 doc/src/sgml/client-auth.sgml          |  10 ++
 doc/src/sgml/func.sgml                 |   5 +-
 src/backend/catalog/system_views.sql   |   6 ++
 src/backend/libpq/hba.c                |  31 +++---
 src/backend/utils/adt/hbafuncs.c       | 136 +++++++++++++++++++++++++
 src/include/catalog/pg_proc.dat        |   7 ++
 src/include/libpq/hba.h                |   1 +
 src/test/regress/expected/rules.out    |   6 ++
 src/test/regress/expected/sysviews.out |  16 ++-
 src/test/regress/sql/sysviews.sql      |   6 +-
 11 files changed, 311 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 94f01e4099..75fedfa07e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -9591,6 +9591,11 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
       <entry>summary of client authentication configuration file contents</entry>
      </row>
 
+     <row>
+      <entry><link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link></entry>
+      <entry>summary of client user name mapping configuration file contents</entry>
+     </row>
+
      <row>
       <entry><link linkend="view-pg-indexes"><structname>pg_indexes</structname></link></entry>
       <entry>indexes</entry>
@@ -10589,6 +10594,109 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
   </para>
  </sect1>
 
+ <sect1 id="view-pg-ident-file-mappings">
+  <title><structname>pg_ident_file_mappings</structname></title>
+
+  <indexterm zone="view-pg-ident-file-mappings">
+   <primary>pg_ident_file_mappings</primary>
+  </indexterm>
+
+  <para>
+   The view <structname>pg_ident_file_mappings</structname> provides a summary
+   of the contents of the client user name mapping configuration file,
+   <link linkend="auth-username-maps"><filename>pg_ident.conf</filename></link>.
+   A row appears in this view for each
+   non-empty, non-comment line in the file, with annotations indicating
+   whether the rule could be applied successfully.
+  </para>
+
+  <para>
+   This view can be helpful for checking whether planned changes in the
+   authentication configuration file will work, or for diagnosing a previous
+   failure.  Note that this view reports on the <emphasis>current</emphasis>
+   contents of the file, not on what was last loaded by the server.
+  </para>
+
+  <para>
+   By default, the <structname>pg_ident_file_mappings</structname> view can be
+   read only by superusers.
+  </para>
+
+  <table>
+   <title><structname>pg_ident_file_mappings</structname> Columns</title> <tgroup
+   cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       Column Type
+      </para>
+      <para>
+       Description
+      </para></entry>
+     </row>
+    </thead>
+
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this rule in <filename>pg_ident.conf</filename>
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>map_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the map
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>sys_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Detected user name of the client
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>pg_username</structfield> <type>text</type>
+      </para>
+      <para>
+       Requested PostgreSQL user name
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>error</structfield> <type>text</type>
+      </para>
+      <para>
+       If not null, an error message indicating why this line could not be
+       processed
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+  <para>
+   Usually, a row reflecting an incorrect entry will have values for only
+   the <structfield>line_number</structfield> and <structfield>error</structfield> fields.
+  </para>
+
+  <para>
+   See <xref linkend="client-authentication"/> for more information about
+   client authentication configuration.
+  </para>
+ </sect1>
+
  <sect1 id="view-pg-indexes">
   <title><structname>pg_indexes</structname></title>
 
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 02f0489112..142b0affcb 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -896,6 +896,16 @@ mymap   /^(.*)@otherdomain\.com$   guest
    -HUP</literal>) to make it re-read the file.
   </para>
 
+  <para>
+   The system view
+   <link linkend="view-pg-ident-file-mappings"><structname>pg_ident_file_mappings</structname></link>
+   can be helpful for pre-testing changes to the
+   <filename>pg_ident.conf</filename> file, or for diagnosing problems if
+   loading of the file did not have the desired effects.  Rows in the view with
+   non-null <structfield>error</structfield> fields indicate problems in the
+   corresponding lines of the file.
+  </para>
+
   <para>
    A <filename>pg_ident.conf</filename> file that could be used in
    conjunction with the <filename>pg_hba.conf</filename> file in <xref
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 8a802fb225..b32cc61886 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -25475,8 +25475,9 @@ SELECT collation for ('foo' COLLATE "de_DE");
         sending a <systemitem>SIGHUP</systemitem> signal to the postmaster
         process, which in turn sends <systemitem>SIGHUP</systemitem> to each
         of its children.) You can use the
-        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link> and
-        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> views
+        <link linkend="view-pg-file-settings"><structname>pg_file_settings</structname></link>,
+        <link linkend="view-pg-hba-file-rules"><structname>pg_hba_file_rules</structname></link> and
+        <link linkend="view-pg-hba-file-rules"><structname>pg_ident_file_mappings</structname></link> views
         to check the configuration files for possible errors, before reloading.
        </para></entry>
       </row>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 9570a53e7b..9eaa51df29 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -617,6 +617,12 @@ CREATE VIEW pg_hba_file_rules AS
 REVOKE ALL ON pg_hba_file_rules FROM PUBLIC;
 REVOKE EXECUTE ON FUNCTION pg_hba_file_rules() FROM PUBLIC;
 
+CREATE VIEW pg_ident_file_mappings AS
+   SELECT * FROM pg_ident_file_mappings() AS A;
+
+REVOKE ALL ON pg_ident_file_mappings FROM PUBLIC;
+REVOKE EXECUTE ON FUNCTION pg_ident_file_mappings() FROM PUBLIC;
+
 CREATE VIEW pg_timezone_abbrevs AS
     SELECT * FROM pg_timezone_abbrevs();
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 673135144d..556f473b41 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -887,25 +887,22 @@ do { \
 } while (0)
 
 /*
- * Macros for handling pg_ident problems.
- * Much as above, but currently the message level is hardwired as LOG
- * and there is no provision for an err_msg string.
+ * Macros for handling pg_ident problems, similar as above.
  *
  * IDENT_FIELD_ABSENT:
- * Log a message and exit the function if the given ident field ListCell is
- * not populated.
+ * Reports when the given ident field ListCell is not populated.
  *
  * IDENT_MULTI_VALUE:
- * Log a message and exit the function if the given ident token List has more
- * than one element.
+ * Reports when the given ident token List has more than one element.
  */
 #define IDENT_FIELD_ABSENT(field) \
 do { \
 	if (!field) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
 						IdentFileName, line_num))); \
+		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
 } while (0)
@@ -913,11 +910,12 @@ do { \
 #define IDENT_MULTI_VALUE(tokens) \
 do { \
 	if (tokens->length > 1) { \
-		ereport(LOG, \
+		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
 							line_num, IdentFileName))); \
+		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
 } while (0)
@@ -2306,7 +2304,8 @@ load_hba(void)
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
  *
- * If parsing fails, log a message and return NULL.
+ * If parsing fails, log a message at ereport level elevel, store an error
+ * string in tok_line->err_msg and return NULL.
  *
  * If ident_user is a regular expression (ie. begins with a slash), it is
  * compiled and stored in IdentLine structure.
@@ -2315,10 +2314,11 @@ load_hba(void)
  * to have set a memory context that will be reset if this function returns
  * NULL.
  */
-static IdentLine *
-parse_ident_line(TokenizedAuthLine *tok_line)
+IdentLine *
+parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
 	List	   *tokens;
 	AuthToken  *token;
@@ -2372,11 +2372,14 @@ parse_ident_line(TokenizedAuthLine *tok_line)
 			char		errstr[100];
 
 			pg_regerror(r, &parsedline->re, errstr, sizeof(errstr));
-			ereport(LOG,
+			ereport(elevel,
 					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
 					 errmsg("invalid regular expression \"%s\": %s",
 							parsedline->ident_user + 1, errstr)));
 
+			*err_msg = psprintf("invalid regular expression \"%s\": %s",
+							   parsedline->ident_user + 1, errstr);
+
 			pfree(wstr);
 			return NULL;
 		}
@@ -2627,7 +2630,7 @@ load_ident(void)
 			continue;
 		}
 
-		if ((newline = parse_ident_line(tok_line)) == NULL)
+		if ((newline = parse_ident_line(tok_line, LOG)) == NULL)
 		{
 			/* Parse error; remember there's trouble */
 			ok = false;
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index f46cd935a1..1970b4c497 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -28,6 +28,9 @@ static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 						  int lineno, HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
+static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int lineno, IdentLine *ident, const char *err_msg);
+static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
 /*
@@ -426,3 +429,136 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 
 	PG_RETURN_NULL();
 }
+
+/* Number of columns in pg_ident_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+
+/*
+ * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
+ * tuplestore
+ *
+ * tuple_store: where to store data
+ * tupdesc: tuple descriptor for the view
+ * lineno: pg_hba.conf line number (must always be valid)
+ * ident: parsed line data (can be NULL, in which case err_msg should be set)
+ * err_msg: error message (NULL if none)
+ *
+ * Note: leaks memory, but we don't care since this is run in a short-lived
+ * memory context.
+ */
+static void
+fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+				int lineno, IdentLine *ident, const char *err_msg)
+{
+	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
+	HeapTuple	tuple;
+	int			index;
+
+	Assert(tupdesc->natts == NUM_PG_IDENT_FILE_MAPPINGS_ATTS);
+
+	memset(values, 0, sizeof(values));
+	memset(nulls, 0, sizeof(nulls));
+	index = 0;
+
+	/* line_number */
+	values[index++] = Int32GetDatum(lineno);
+
+	if (ident != NULL)
+	{
+		values[index++] = CStringGetTextDatum(ident->usermap);
+		values[index++] = CStringGetTextDatum(ident->ident_user);
+		values[index++] = CStringGetTextDatum(ident->pg_role);
+	}
+	else
+	{
+		/* no parsing result, so set relevant fields to nulls */
+		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+	}
+
+	/* error */
+	if (err_msg)
+		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = CStringGetTextDatum(err_msg);
+	else
+		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 1] = true;
+
+	tuple = heap_form_tuple(tupdesc, values, nulls);
+	tuplestore_puttuple(tuple_store, tuple);
+}
+
+/*
+ * Read the pg_ident.conf file and fill the tuplestore with view records.
+ */
+static void
+fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
+{
+	FILE	   *file;
+	List	   *ident_lines = NIL;
+	ListCell   *line;
+	MemoryContext linecxt;
+	MemoryContext identcxt;
+	MemoryContext oldcxt;
+
+	/*
+	 * In the unlikely event that we can't open pg_hba.conf, we throw an
+	 * error, rather than trying to report it via some sort of view entry.
+	 * (Most other error conditions should result in a message in a view
+	 * entry.)
+	 */
+	file = AllocateFile(IdentFileName, "r");
+	if (file == NULL)
+		ereport(ERROR,
+				(errcode_for_file_access(),
+				 errmsg("could not open usermap file \"%s\": %m",
+						IdentFileName)));
+
+	linecxt = tokenize_auth_file(HbaFileName, file, &ident_lines, DEBUG3);
+	FreeFile(file);
+
+	/* Now parse all the lines */
+	identcxt = AllocSetContextCreate(CurrentMemoryContext,
+								   "ident parser context",
+								   ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(identcxt);
+	foreach(line, ident_lines)
+	{
+		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line);
+		IdentLine   *identline = NULL;
+
+		/* don't parse lines that already have errors */
+		if (tok_line->err_msg == NULL)
+			identline = parse_ident_line(tok_line, DEBUG3);
+
+		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
+					  tok_line->err_msg);
+	}
+
+	/* Free tokenizer memory */
+	MemoryContextDelete(linecxt);
+	/* Free parse_ident_line memory */
+	MemoryContextSwitchTo(oldcxt);
+	MemoryContextDelete(identcxt);
+}
+
+/*
+ * SQL-accessible SRF to return all the entries in the pg_ident.conf file.
+ */
+Datum
+pg_ident_file_mappings(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsi;
+
+	/*
+	 * Build tuplestore to hold the result rows.  We must use the Materialize
+	 * mode to be safe against HBA file changes while the cursor is open.
+	 * It's also more efficient than having to look up our current position in
+	 * the parsed list every time.
+	 */
+	SetSingleFuncCall(fcinfo, 0);
+
+	/* Fill the tuplestore */
+	rsi = (ReturnSetInfo *) fcinfo->resultinfo;
+	fill_ident_view(rsi->setResult, rsi->setDesc);
+
+	PG_RETURN_NULL();
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 5e612a6b67..efd48741d8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6115,6 +6115,13 @@
   proargmodes => '{o,o,o,o,o,o,o,o,o}',
   proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
+{ oid => '9556', descr => 'show pg_ident.conf mappings',
+  proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
+  provolatile => 'v', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o}',
+  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 13ecb329f8..90036f7bcd 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -171,6 +171,7 @@ extern int	check_usermap(const char *usermap_name,
 						  const char *pg_role, const char *auth_user,
 						  bool case_sensitive);
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
+extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
 										List **tok_lines, int elevel);
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 27d19b4bf1..5a20e5a7e1 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1347,6 +1347,12 @@ pg_hba_file_rules| SELECT a.line_number,
     a.options,
     a.error
    FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.line_number,
+    a.map_name,
+    a.sys_name,
+    a.pg_username,
+    a.error
+   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out
index 442eeb1e3f..b81c0dcd43 100644
--- a/src/test/regress/expected/sysviews.out
+++ b/src/test/regress/expected/sysviews.out
@@ -49,10 +49,18 @@ select count(*) >= 0 as ok from pg_file_settings;
 (1 row)
 
 -- There will surely be at least one rule
-select count(*) > 0 as ok from pg_hba_file_rules;
- ok 
-----
- t
+select count(*) > 0 as ok, count(*) FILTER (WHERE error IS NOT NULL) = 0 AS no_err
+  from pg_hba_file_rules;
+ ok | no_err 
+----+--------
+ t  | t
+(1 row)
+
+select count(*) >= 0 as ok, count(*) FILTER (WHERE error IS NOT NULL) = 0 AS no_err
+  from pg_ident_file_mappings;
+ ok | no_err 
+----+--------
+ t  | t
 (1 row)
 
 -- There will surely be at least one active lock
diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql
index 4980f07be2..4c36f704d0 100644
--- a/src/test/regress/sql/sysviews.sql
+++ b/src/test/regress/sql/sysviews.sql
@@ -26,7 +26,11 @@ select count(*) = 0 as ok from pg_cursors;
 select count(*) >= 0 as ok from pg_file_settings;
 
 -- There will surely be at least one rule
-select count(*) > 0 as ok from pg_hba_file_rules;
+select count(*) > 0 as ok, count(*) FILTER (WHERE error IS NOT NULL) = 0 AS no_err
+  from pg_hba_file_rules;
+
+select count(*) >= 0 as ok, count(*) FILTER (WHERE error IS NOT NULL) = 0 AS no_err
+  from pg_ident_file_mappings;
 
 -- There will surely be at least one active lock
 select count(*) > 0 as ok from pg_locks;
-- 
2.33.1

v5-0002-Allow-file-inclusion-in-pg_hba-and-pg_ident-files.patchtext/plain; charset=us-asciiDownload
From 84759803f64d3532341d0bf877e2a3a1e31554f2 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 15:45:26 +0800
Subject: [PATCH v5 2/3] Allow file inclusion in pg_hba and pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml             |  48 +++++-
 doc/src/sgml/client-auth.sgml          |  34 +++-
 src/backend/libpq/hba.c                | 229 +++++++++++++++++++------
 src/backend/libpq/pg_hba.conf.sample   |   8 +-
 src/backend/libpq/pg_ident.conf.sample |   8 +-
 src/backend/utils/adt/hbafuncs.c       |  53 ++++--
 src/include/catalog/pg_proc.dat        |  12 +-
 src/include/libpq/hba.h                |   2 +
 src/test/regress/expected/rules.out    |  12 +-
 9 files changed, 321 insertions(+), 85 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 75fedfa07e..bd1c9a8d21 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -10496,12 +10496,31 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule in the given file_name
       </para></entry>
      </row>
 
@@ -10637,6 +10656,33 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this mapping if the mapping is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this mapping
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this mapping in the given file_name
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 142b0affcb..e1d0e103b3 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,17 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,6 +112,7 @@
   <para>
    A record can have several formats:
 <synopsis>
+include       <replaceable>file</replaceable>
 local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
 host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
@@ -118,6 +128,15 @@ hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceabl
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -835,8 +854,9 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -847,6 +867,14 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   The lines can record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> directive and the file to be included.  The file
+   can be a relative of absolute path, and can be double quoted if needed.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 556f473b41..1551b34c53 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -68,6 +68,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -112,10 +118,16 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
 
 
 /*
@@ -355,36 +367,11 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, outer_filename,
+							 elevel, err_msg, &inc_fullname);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
 	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
@@ -425,11 +412,36 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a decicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -438,30 +450,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -522,29 +526,76 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+				char	   *inc_fullname;
+				FILE	   *inc_file;
+
+				inc_filename = second->string;
+
+				inc_file = open_inc_file(IncludedAuthFile, inc_filename,
+										 filename, elevel, &err_msg,
+										 &inc_fullname);
+
+				/*
+				 * The included file could be open, now recursively process it.
+				 * Errors will be reported in the general TokenizedAuthLine
+				 * processing.
+				 */
+				if (inc_file != NULL)
+				{
+					tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+											   tok_lines, elevel);
+
+					FreeFile(inc_file);
+					pfree(inc_fullname);
+
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+				else
+				{
+					/* We should got an error */
+					Assert(err_msg != NULL);
+				}
+			}
 		}
 
+		/* General processing: emit line to the TokenizedAuthLine */
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
+
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -859,7 +910,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -879,7 +930,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -901,7 +952,7 @@ do { \
 		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
-						IdentFileName, line_num))); \
+						tok_line->file_name, line_num))); \
 		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -914,7 +965,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, tok_line->file_name))); \
 		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -937,6 +988,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -951,6 +1003,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -1677,6 +1730,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -2299,6 +2353,67 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		switch (kind)
+		{
+			case SecondaryAuthFile:
+				msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+				msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+				break;
+			case IncludedAuthFile:
+				msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+				msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+				break;
+			default:
+				elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+				break;
+		}
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg(msglog, inc_filename, *inc_fullname)));
+		*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+							strerror(save_errno));
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..0b6589a7b9 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,6 +9,7 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
+# include       FILE
 # local         DATABASE  USER  METHOD  [OPTIONS]
 # host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 # hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
@@ -18,7 +19,12 @@
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", it's not a mapping record but a directive to
+# include records from another file, specified in the field.  FILE is the file
+# to include.  It can be specified with a relative or absolute path, and can be
+# double quoted if it contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..138359cf03 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,18 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
+# include  FILE
 # MAPNAME  SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", it's not an authentication record but a
+# directive to include records from another file, specified in the field.  FILE
+# is the file to include.  It can be specified with a relative or absolute
+# path, and can be double quoted if it contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 1970b4c497..33cf5fb954 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,9 +26,11 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int mapping_number, const char *filename,
 							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -174,7 +176,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +196,13 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +346,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +369,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,8 +404,12 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -430,8 +445,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,13 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +496,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +518,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,8 +553,13 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-					  tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index efd48741d8..04cab629f5 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6111,16 +6111,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '9556', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 90036f7bcd..59f6faf9f8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -155,6 +156,7 @@ typedef struct AuthToken
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 5a20e5a7e1..e2c6aa8ffb 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,9 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1348,15 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.33.1

v5-0003-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From 791e7c62e81523db751c92fde2b9399b2c0d3112 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v5 3/3] POC: Add a pg_hba_matches() function.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 81bac6f581..049cdabc81 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 1551b34c53..a757a54c87 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -26,6 +26,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -41,6 +42,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2835,3 +2837,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 04cab629f5..ea7d7c7eff 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6122,6 +6122,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.33.1

#29Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#28)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Mar 28, 2022 at 04:33:30PM +0800, Julien Rouhaud wrote:

Ok, v5 attached without the TAP tests and updated sysviews tests.

The update of the query related to pg_hba_file_rules in the regression
tests was independant, so I have split and applied that first, as of
091a971.

Now, for the rest, I have found one place in the docs that had an
incorrect link, two incorrect comments (aka
s/pg_hba.conf/pg_ident.conf/), and the indentation was a bit off.
Anyway, all that was cosmetic, so applied after adjusting all those
things.
--
Michael

#30Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#29)
2 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Tue, Mar 29, 2022 at 10:21:36AM +0900, Michael Paquier wrote:

On Mon, Mar 28, 2022 at 04:33:30PM +0800, Julien Rouhaud wrote:

Ok, v5 attached without the TAP tests and updated sysviews tests.

The update of the query related to pg_hba_file_rules in the regression
tests was independant, so I have split and applied that first, as of
091a971.

Ok.

Now, for the rest, I have found one place in the docs that had an
incorrect link, two incorrect comments (aka
s/pg_hba.conf/pg_ident.conf/), and the indentation was a bit off.
Anyway, all that was cosmetic, so applied after adjusting all those
things.

Thanks!

I'm attaching a rebase of the rest of the patchset. Note that I also added
support for an "include_dir" directive, as it's also something that would be
quite helpful (and very welcome for my $work use case).

Attachments:

v6-0001-Allow-file-inclusion-in-pg_hba-and-pg_ident-files.patchtext/plain; charset=us-asciiDownload
From e5dc352cd2eb8cff24714930338adb0b2d20ff94 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 15:45:26 +0800
Subject: [PATCH v6 1/2] Allow file inclusion in pg_hba and pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml             |  48 +++-
 doc/src/sgml/client-auth.sgml          |  48 +++-
 src/backend/libpq/hba.c                | 343 +++++++++++++++++++++----
 src/backend/libpq/pg_hba.conf.sample   |  10 +-
 src/backend/libpq/pg_ident.conf.sample |  12 +-
 src/backend/utils/adt/hbafuncs.c       |  51 +++-
 src/include/catalog/pg_proc.dat        |  11 +-
 src/include/libpq/hba.h                |   2 +
 src/test/regress/expected/rules.out    |  12 +-
 9 files changed, 453 insertions(+), 84 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 7f4f79d1b5..2b5b7ef5d6 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -10496,12 +10496,31 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule in the given file_name
       </para></entry>
      </row>
 
@@ -10636,6 +10655,33 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this mapping if the mapping is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this mapping
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this mapping in the given file_name
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 142b0affcb..4e1438476e 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,21 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> or <literal>include_dir</literal> directive and
+   the file or directory to be included.  The file or directory can be a
+   relative of absolute path, and can be double quoted if needed.  For the
+   <literal>include_dir</literal> form, all files not starting with a
+   <literal>.</literal> and ending with <literal>.conf</literal> will be
+   included.
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,6 +116,8 @@
   <para>
    A record can have several formats:
 <synopsis>
+include       <replaceable>file</replaceable>
+include_dir   <replaceable>directory</replaceable>
 local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
 host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
@@ -118,6 +133,26 @@ hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceabl
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -835,8 +870,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -847,6 +884,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or an authentication rules, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index f8393ca8ed..5d0caa587b 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -21,6 +21,7 @@
 #include <fcntl.h>
 #include <sys/param.h>
 #include <sys/socket.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -68,6 +69,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -112,10 +119,19 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename,
+									   const char *outer_filename, int elevel,
+									   MemoryContext linecxt, List **tok_lines);
 
 
 /*
@@ -355,36 +371,11 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, outer_filename,
+							 elevel, err_msg, &inc_fullname);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
 	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
@@ -425,11 +416,36 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a decicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -438,30 +454,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -522,29 +530,151 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, filename,
+										  elevel, linecxt, tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	   *dir_name;
+				DIR		   *d;
+				struct dirent *de;
+				StringInfoData err_buf;
+
+				dir_name = second->string;
+				d = AllocateDir(dir_name);
+				if (d == NULL)
+				{
+					ereport(elevel,
+							(errcode_for_file_access(),
+							 errmsg("could not open directory \"%s\": %m",
+									dir_name)));
+					err_msg = psprintf("coud not open directory \"%s\": %m",
+									   dir_name);
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				while ((de = ReadDir(d, dir_name)) != NULL)
+				{
+					struct stat st;
+					char		inc_filename[MAXPGPATH];
+
+					/*
+					 * Only parse files with names ending in ".conf".
+					 * Explicitly reject files starting with ".".  This
+					 * excludes things like "." and "..", as well as typical
+					 * hidden files, backup files, and editor debris.
+					 */
+					if (strlen(de->d_name) < 6)
+						continue;
+					if (de->d_name[0] == '.')
+						continue;
+					if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+						continue;
+
+					join_path_components(inc_filename, dir_name, de->d_name);
+					canonicalize_path(inc_filename);
+					if (stat(inc_filename, &st) == 0)
+					{
+						/* Ignore directories. */
+						if (S_ISDIR(st.st_mode))
+							continue;
+
+						/*
+						 * err_msg is used here as a temp buffer, it will be
+						 * overwritten at the end of the loop with the
+						 * cumulated errors, if any.
+						 */
+						err_msg = process_included_authfile(inc_filename,
+													filename, elevel, linecxt,
+													tok_lines);
+
+						/* Cumulate errors if any. */
+						if (err_msg)
+						{
+							if (err_buf.len > 0)
+								appendStringInfoChar(&err_buf, '\n');
+							appendStringInfoString(&err_buf, err_msg);
+						}
+					}
+					else
+					{
+						ereport(elevel,
+								(errcode_for_file_access(),
+								 errmsg("could not stat file \"%s\": %m",
+									 filename)));
+
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfo(&err_buf,
+										 "could not stat file \"%s\": %m",
+										 inc_filename);
+					}
+				}
+				FreeDir(d);
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
+
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -859,7 +989,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -879,7 +1009,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -901,7 +1031,7 @@ do { \
 		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
-						IdentFileName, line_num))); \
+						tok_line->file_name, line_num))); \
 		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -914,7 +1044,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, tok_line->file_name))); \
 		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -937,6 +1067,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -951,6 +1082,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -1677,6 +1809,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -2299,6 +2432,102 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		switch (kind)
+		{
+			case SecondaryAuthFile:
+				msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+				msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+				break;
+			case IncludedAuthFile:
+				msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+				msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+				break;
+			default:
+				elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+				break;
+		}
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg(msglog, inc_filename, *inc_fullname)));
+		*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+							strerror(save_errno));
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ */
+static char *
+process_included_authfile(const char *inc_filename, const char *outer_filename,
+						  int elevel, MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, outer_filename,
+							 elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		/* open_inc_file should have reported an error. */
+		Assert(err_msg != NULL);
+		return err_msg;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..0050e24186 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,6 +9,8 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
+# include       FILE
+# include_dir   DIRECTORY
 # local         DATABASE  USER  METHOD  [OPTIONS]
 # host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 # hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
@@ -18,7 +20,13 @@
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include" or "include_dir", it's not a mapping record but a directive to
+# include records from other file(s), as specified in the field.  FILE is the
+# file to include, and DIR is the directory containing the file(s) to include
+# It can be specified with a relative or absolute path, and can be double
+# quoted if it contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..9b700d9989 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,20 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include       FILE
+# include_dir   FILE
+# MAPNAME       SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include" or "include_dir", it's not an authentication
+# record but a directive to include records from other file(s), specified in
+# the field.  FILE is the file to include and DIR is the directory containing
+# the file(s) to include.  It can be specified with a relative or absolute
+# path, and can be double quoted if it contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 9fe7b62c9a..4d902927e0 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,9 +26,11 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int mapping_number, const char *filename,
 							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -174,7 +176,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +196,13 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +346,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +369,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,8 +404,12 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -430,8 +445,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,13 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +496,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +518,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,7 +553,12 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->file_name, tok_line->line_num, identline,
 						tok_line->err_msg);
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 01e1dd4d6d..467f6f1293 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6111,15 +6111,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '9556', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 90036f7bcd..59f6faf9f8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -155,6 +156,7 @@ typedef struct AuthToken
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 423b9b99fb..e00185eb6d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,9 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1348,15 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.33.1

v6-0002-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From 0a3ae820ce838fc829cd735a3e80a87696bc02a3 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v6 2/2] POC: Add a pg_hba_matches() function.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 81bac6f581..049cdabc81 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 5d0caa587b..b44b036d05 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -27,6 +27,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -42,6 +43,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2949,3 +2951,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 467f6f1293..2b6fa8aeba 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6122,6 +6122,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.33.1

#31Julien Rouhaud
rjuju123@gmail.com
In reply to: Julien Rouhaud (#30)
2 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Tue, Mar 29, 2022 at 04:14:54PM +0800, Julien Rouhaud wrote:

I'm attaching a rebase of the rest of the patchset. Note that I also added
support for an "include_dir" directive, as it's also something that would be
quite helpful (and very welcome for my $work use case).

The cfbot reports that the patch doesn't apply anymore, rebased v7 attached.

Attachments:

v7-0001-Allow-file-inclusion-in-pg_hba-and-pg_ident-files.patchtext/plain; charset=us-asciiDownload
From 04943f3ff8dfb8efa5e538e0d9524fb041c3b39b Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 21 Feb 2022 15:45:26 +0800
Subject: [PATCH v7 1/2] Allow file inclusion in pg_hba and pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/catalogs.sgml             |  48 +++-
 doc/src/sgml/client-auth.sgml          |  48 +++-
 src/backend/libpq/hba.c                | 343 +++++++++++++++++++++----
 src/backend/libpq/pg_hba.conf.sample   |  10 +-
 src/backend/libpq/pg_ident.conf.sample |  12 +-
 src/backend/utils/adt/hbafuncs.c       |  51 +++-
 src/include/catalog/pg_proc.dat        |  11 +-
 src/include/libpq/hba.h                |   2 +
 src/test/regress/expected/rules.out    |  12 +-
 9 files changed, 453 insertions(+), 84 deletions(-)

diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a533a2153e..b44e62d388 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -10554,12 +10554,31 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule in the given file_name
       </para></entry>
      </row>
 
@@ -10694,6 +10713,33 @@ SCRAM-SHA-256$<replaceable>&lt;iteration count&gt;</replaceable>:<replaceable>&l
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number, in priority order, of this mapping if the mapping is valid,
+       otherwise null
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       File name of this mapping
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>line_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Line number of this mapping in the given file_name
+      </para></entry>
+     </row>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index b2a459fb0d..f7d871e660 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,21 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication rule.
+   Inclusion records specifies files that can be included, which contains
+   additional records.  The records will be inserted in lieu of the inclusion
+   records.  Those records only contains two fields: the
+   <literal>include</literal> or <literal>include_dir</literal> directive and
+   the file or directory to be included.  The file or directory can be a
+   relative of absolute path, and can be double quoted if needed.  For the
+   <literal>include_dir</literal> form, all files not starting with a
+   <literal>.</literal> and ending with <literal>.conf</literal> will be
+   included.
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,6 +116,8 @@
   <para>
    A record can have several formats:
 <synopsis>
+include       <replaceable>file</replaceable>
+include_dir   <replaceable>directory</replaceable>
 local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
 host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
@@ -118,6 +133,26 @@ hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceabl
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>.
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -835,8 +870,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -847,6 +884,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or an authentication rule, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 327a4b42af..56b6cec9d5 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -21,6 +21,7 @@
 #include <fcntl.h>
 #include <sys/param.h>
 #include <sys/socket.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -68,6 +69,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -112,10 +119,19 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename,
+									   const char *outer_filename, int elevel,
+									   MemoryContext linecxt, List **tok_lines);
 
 
 /*
@@ -355,36 +371,11 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, outer_filename,
+							 elevel, err_msg, &inc_fullname);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
 	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
@@ -425,11 +416,36 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a decicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -438,30 +454,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -522,29 +530,151 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, filename,
+										  elevel, linecxt, tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	   *dir_name;
+				DIR		   *d;
+				struct dirent *de;
+				StringInfoData err_buf;
+
+				dir_name = second->string;
+				d = AllocateDir(dir_name);
+				if (d == NULL)
+				{
+					ereport(elevel,
+							(errcode_for_file_access(),
+							 errmsg("could not open directory \"%s\": %m",
+									dir_name)));
+					err_msg = psprintf("coud not open directory \"%s\": %m",
+									   dir_name);
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				while ((de = ReadDir(d, dir_name)) != NULL)
+				{
+					struct stat st;
+					char		inc_filename[MAXPGPATH];
+
+					/*
+					 * Only parse files with names ending in ".conf".
+					 * Explicitly reject files starting with ".".  This
+					 * excludes things like "." and "..", as well as typical
+					 * hidden files, backup files, and editor debris.
+					 */
+					if (strlen(de->d_name) < 6)
+						continue;
+					if (de->d_name[0] == '.')
+						continue;
+					if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+						continue;
+
+					join_path_components(inc_filename, dir_name, de->d_name);
+					canonicalize_path(inc_filename);
+					if (stat(inc_filename, &st) == 0)
+					{
+						/* Ignore directories. */
+						if (S_ISDIR(st.st_mode))
+							continue;
+
+						/*
+						 * err_msg is used here as a temp buffer, it will be
+						 * overwritten at the end of the loop with the
+						 * cumulated errors, if any.
+						 */
+						err_msg = process_included_authfile(inc_filename,
+													filename, elevel, linecxt,
+													tok_lines);
+
+						/* Cumulate errors if any. */
+						if (err_msg)
+						{
+							if (err_buf.len > 0)
+								appendStringInfoChar(&err_buf, '\n');
+							appendStringInfoString(&err_buf, err_msg);
+						}
+					}
+					else
+					{
+						ereport(elevel,
+								(errcode_for_file_access(),
+								 errmsg("could not stat file \"%s\": %m",
+									 filename)));
+
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfo(&err_buf,
+										 "could not stat file \"%s\": %m",
+										 inc_filename);
+					}
+				}
+				FreeDir(d);
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
+
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -859,7 +989,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -879,7 +1009,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -901,7 +1031,7 @@ do { \
 		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry in file \"%s\" at end of line %d", \
-						IdentFileName, line_num))); \
+						tok_line->file_name, line_num))); \
 		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -914,7 +1044,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, tok_line->file_name))); \
 		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -937,6 +1067,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -951,6 +1082,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -1675,6 +1807,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -2296,6 +2429,102 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		switch (kind)
+		{
+			case SecondaryAuthFile:
+				msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+				msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+				break;
+			case IncludedAuthFile:
+				msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+				msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+				break;
+			default:
+				elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+				break;
+		}
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg(msglog, inc_filename, *inc_fullname)));
+		*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+							strerror(save_errno));
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ */
+static char *
+process_included_authfile(const char *inc_filename, const char *outer_filename,
+						  int elevel, MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, outer_filename,
+							 elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		/* open_inc_file should have reported an error. */
+		Assert(err_msg != NULL);
+		return err_msg;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..0050e24186 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,6 +9,8 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
+# include       FILE
+# include_dir   DIRECTORY
 # local         DATABASE  USER  METHOD  [OPTIONS]
 # host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 # hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
@@ -18,7 +20,13 @@
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include" or "include_dir", it's not a mapping record but a directive to
+# include records from other file(s), as specified in the field.  FILE is the
+# file to include, and DIR is the directory containing the file(s) to include
+# It can be specified with a relative or absolute path, and can be double
+# quoted if it contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..9b700d9989 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,20 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include       FILE
+# include_dir   FILE
+# MAPNAME       SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include" or "include_dir", it's not an authentication
+# record but a directive to include records from other file(s), specified in
+# the field.  FILE is the file to include and DIR is the directory containing
+# the file(s) to include.  It can be specified with a relative or absolute
+# path, and can be double quoted if it contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 9fe7b62c9a..4d902927e0 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,9 +26,11 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
+							int mapping_number, const char *filename,
 							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -174,7 +176,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +196,13 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +346,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +369,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,8 +404,12 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -430,8 +445,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,13 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +496,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +518,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,7 +553,12 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
+		/* No error, set a rule number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->file_name, tok_line->line_num, identline,
 						tok_line->err_msg);
 	}
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index babe16f00a..65bfd32753 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6128,15 +6128,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 90036f7bcd..59f6faf9f8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -155,6 +156,7 @@ typedef struct AuthToken
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 21effe8315..11ffe1743b 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,9 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1348,15 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.33.1

v7-0002-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From 429b429b32b08024ea04cd838c63c9f9298ae0a2 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v7 2/2] POC: Add a pg_hba_matches() function.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 73da687d5d..bfee72f705 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 56b6cec9d5..09554a9bc6 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -27,6 +27,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -42,6 +43,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2946,3 +2948,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 65bfd32753..2c65ee2019 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6139,6 +6139,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.33.1

#32Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#31)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, May 18, 2022 at 03:12:45PM +0800, Julien Rouhaud wrote:

The cfbot reports that the patch doesn't apply anymore, rebased v7 attached.

+       switch (kind)
+       {
+           case SecondaryAuthFile:
+               msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+               msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+               break;
+           case IncludedAuthFile:
+               msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+               msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+               break;
+           default:
+               elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+               break;
+       }
I don't think that HbaIncludeKind is necessary, considering that we
could rely on the file name to provide enough context about the type
involved in the error string generated.  The "default" clause in the
switch could just be removed, btw, to generate warnings if a new value
is added in the kind enum.
+       /* relative path is relative to dir of calling file */
There are many relatives here.  It seems to me that this is the kind
of behavior we should document precisely (and test!).  I am
understanding the following for cascading configurations:
- pg_hba.conf has "include_dir path1".
- path1/ has file1.conf that has "include file2.conf".  This means
that file2.conf has to be also included in path1/.

postmaster.c and postinit.c do that:
/*
* Load configuration files for client authentication.
*/
if (!load_hba())
{
/*
* It makes no sense to continue if we fail to load the HBA file,
* since there is no way to connect to the database in this case.
*/
ereport(FATAL,
(errmsg("could not load pg_hba.conf")));
}
This could be confusing as a different file may fail to load, while
pg_hba.conf was able to do its work outside an include clause.

How does include_dir work with the ordering of the HBA entries across
multiple files? The set of HBA rules are very sensitive to their
ordering, and ReadDir() depends on readdir(), which provides a list
of files depending on its FS implementation. That does not seem like
a sane concept to me. I can this this order (tracked by rule_number)
being strictly enforced when it comes to the loading of files, though,
so I would recommend to focus on the implementation of "include"
rather than "include_dir".

+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
This is a very important field.  I think that this explanation should
be extended and explain why relying on this number counts (aka this is
the order of the rules loaded across files).  Btw, this could be added
as a separate patch, even if this maps to the line number when it
comes to the ordering.
+# include       FILE
+# include_dir   FILE
You mean s/FILE/DIRECTORY/ for include_dir, I guess?
+                   /*
+                    * Only parse files with names ending in ".conf".
+                    * Explicitly reject files starting with ".". This
+                    * excludes things like "." and "..", as well as typical
+                    * hidden files, backup files, and editor debris.
+                    */
I don't think that there is any need to restrict that to files ending
with .conf.  We don't do that for postgresql.conf's include, for one.

In 0002, pg_hba_matches() had better have some documentation,
explaining for which purpose this function can be used with a short
example (aka for an address and a role, find the matching set of HBA
rules and report their line and file)?

I am not sure to be a huge fan of this implementation, actually. The
function is shaped so as the user provides in input the arguments to
fill hbaPort with, passing it down to check_hba(). This could bite
easily in the future if some of the internal fields filled in by the
HBA load and used by the HBA check change over time, particularly if
this stuff has no tests to provide some validation, though we
discussed that a couple of months ago. Perhaps we should think
harder on this point.

+ char tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
Okay. This is the same way of doing things as network.c or
inet_cidr_ntop.c. Shouldn't we centralize the definition of such a
maximum size instead?

+   if (!load_hba())
+       ereport(ERROR,
+               (errcode(ERRCODE_CONFIG_FILE_ERROR),
+                errmsg("Invalidation auth configuration file")));
This error message sounds wrong to me.  It would be more consistent to
write that as "could not load authentication file" or such.
postinit.c and postmaster.c do that (these error strings become
partially confusing with the possibility to include extra auth files,
actually, on a separate note).
+   if (PG_ARGISNULL(1))
+       ereport(ERROR,
+               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                errmsg("parameter role is mandatory")));
+   port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
This could be moved at the beginning of the function, before
processing the first argument of the function (address) in details.
--
Michael
#33Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#32)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Mon, May 23, 2022 at 03:53:06PM +0900, Michael Paquier wrote:

+       switch (kind)
+       {
+           case SecondaryAuthFile:
+               msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+               msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+               break;
+           case IncludedAuthFile:
+               msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+               msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+               break;
+           default:
+               elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+               break;
+       }
I don't think that HbaIncludeKind is necessary, considering that we
could rely on the file name to provide enough context about the type
involved in the error string generated.

I'm not sure it would always be the case. For instance, why wouldn't someone
use something like

peer all @app_users ident

rather than

@include app_users

or a mix of both?

That being said, I'd gladly drop that enum and only handle a single error
message, as the rest of the error context (including the owning file name and
line) should provide enough information to users.

If so, should I use "included authentication file" everywhere, or use something
else?

The "default" clause in the
switch could just be removed, btw, to generate warnings if a new value
is added in the kind enum.

Right.

+ /* relative path is relative to dir of calling file */
There are many relatives here. It seems to me that this is the kind
of behavior we should document precisely

Agreed. I will try to mimic how it's done for
https://www.postgresql.org/docs/current/config-setting.html#CONFIG-INCLUDES.

(and test!).

Sure. As I said previously I'm waiting for some agreement on the feature (and
the syntax), before adding tests. After that I will definitely include full
test coverage!

I am
understanding the following for cascading configurations:
- pg_hba.conf has "include_dir path1".
- path1/ has file1.conf that has "include file2.conf". This means
that file2.conf has to be also included in path1/.

Right, same rules as for postgresql.conf's include_dir.

postmaster.c and postinit.c do that:
/*
* Load configuration files for client authentication.
*/
if (!load_hba())
{
/*
* It makes no sense to continue if we fail to load the HBA file,
* since there is no way to connect to the database in this case.
*/
ereport(FATAL,
(errmsg("could not load pg_hba.conf")));
}
This could be confusing as a different file may fail to load, while
pg_hba.conf was able to do its work outside an include clause.

Well, even if included they are still part of the configuration and lead to a
failure to load the main file.

The detail should be provided in the logs so it should disambiguate the
situation. While you're mentioning this message, AFAICS it can already be
entirely wrong as-is, as it doesn't take into account the hba_file
configuration:

ALTER SYSTEM SET hba_file = '/tmp/myfile';

After a restart:
2022-05-23 18:52:08.081 CST [41890] LOG: could not open configuration file
"/tmp/myfile": No such file or directory
2022-05-23 18:52:08.081 CST [41890] FATAL: could not load pg_hba.conf

How does include_dir work with the ordering of the HBA entries across
multiple files?

Bugs apart, same as for postgresql.conf's include_dir.

The set of HBA rules are very sensitive to their
ordering, and ReadDir() depends on readdir(), which provides a list
of files depending on its FS implementation. That does not seem like
a sane concept to me. I can this this order (tracked by rule_number)
being strictly enforced when it comes to the loading of files, though,
so I would recommend to focus on the implementation of "include"
rather than "include_dir".

But the same problem already exists for the postgresql.conf's include_dir.

Having an hba rule masking another isn't really better than having a GUC
value overloaded by another one. Usually people rely on a sane naming (like
01_file.conf and so on) to get a predictable behavior, and we even document
that very thoroughly for the postgresql.conf part. Improving the
documentation, as you already pointed out a bit above, should be enough?

+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
This is a very important field.  I think that this explanation should
be extended and explain why relying on this number counts (aka this is
the order of the rules loaded across files).  Btw, this could be added
as a separate patch, even if this maps to the line number when it
comes to the ordering.

Agreed, I will improve the documentation to outline the importance of that
information.

Do you mean a separate patch to ease review or to eventually commit both
separately? FWIW I don't think that adding any form of inclusion in the hba
files should be done without a way for users to check the results. Any test
facility would also probably rely on this field.

+# include       FILE
+# include_dir   FILE
You mean s/FILE/DIRECTORY/ for include_dir, I guess?

Oops yes.

+                   /*
+                    * Only parse files with names ending in ".conf".
+                    * Explicitly reject files starting with ".". This
+                    * excludes things like "." and "..", as well as typical
+                    * hidden files, backup files, and editor debris.
+                    */
I don't think that there is any need to restrict that to files ending
with .conf.  We don't do that for postgresql.conf's include, for one.

I'm confused. Are you talking about postgresql.conf's include or include_dir
option? I'm pretty sure that I borrowed this logic from
src/backend/utils/misc/guc-file.l / ParseConfigDirectory(), which implements
the include_dir logic for the postgresql.conf file.

In 0002, pg_hba_matches() had better have some documentation,
explaining for which purpose this function can be used with a short
example (aka for an address and a role, find the matching set of HBA
rules and report their line and file)?

I am not sure to be a huge fan of this implementation, actually. The
function is shaped so as the user provides in input the arguments to
fill hbaPort with, passing it down to check_hba(). This could bite
easily in the future if some of the internal fields filled in by the
HBA load and used by the HBA check change over time, particularly if
this stuff has no tests to provide some validation, though we
discussed that a couple of months ago. Perhaps we should think
harder on this point.

First, it's for now only a POC to demonstrate a way to diagnose problems in
those files (thus no doc and obviously no tests). I added some details in my
initial email:

Finally I also added 0003, which is a POC for a new pg_hba_matches()
function, that can help DBA to understand why their configuration isn't
working as they expect. This only to start the discussion on that topic, the
code is for now really hackish, as I don't know how much this is wanted
and/or if some other behavior would be better, and there's also no
documentation or test. The function for now only takes an optional inet
(null means unix socket), the target role and an optional ssl flag and
returns the file, line and raw line matching if any, or null. For instance:

=# select * from pg_hba_matches('127.0.0.1'::inet, 'postgres');
-[ RECORD 1 ]-----------------------------------------------------------------
file_name | /tmp/pgbuild/toto.conf
line_num | 5
raw_line | host all all 127.0.0.1/32 trust

I will put more details and this example in the commit message if you want, but
if no one is interested in that feature I'm ok with discarding it.

I can of course change the implementation, but unless I'm missing something
users will always have to provide all the relevant info to check the behavior
for a specific connection origin?

+ char tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
Okay. This is the same way of doing things as network.c or
inet_cidr_ntop.c. Shouldn't we centralize the definition of such a
maximum size instead?

+   if (!load_hba())
+       ereport(ERROR,
+               (errcode(ERRCODE_CONFIG_FILE_ERROR),
+                errmsg("Invalidation auth configuration file")));
This error message sounds wrong to me.  It would be more consistent to
write that as "could not load authentication file" or such.
postinit.c and postmaster.c do that (these error strings become
partially confusing with the possibility to include extra auth files,
actually, on a separate note).

Oops, I meant "Invalid". I'll Cleanup the error messages and refactor stuff if
this goes beyond the POC stage.

+   if (PG_ARGISNULL(1))
+       ereport(ERROR,
+               (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                errmsg("parameter role is mandatory")));
+   port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
This could be moved at the beginning of the function, before
processing the first argument of the function (address) in details.

I thought it'd be better to handle each argument sequentially for readability,
but I could move all the sanity checks first if you prefer.

#34Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#33)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, May 23, 2022 at 07:43:08PM +0800, Julien Rouhaud wrote:

That being said, I'd gladly drop that enum and only handle a single error
message, as the rest of the error context (including the owning file name and
line) should provide enough information to users.

If so, should I use "included authentication file" everywhere, or use something
else?

With the file name provided the context, "authentication file"?

The detail should be provided in the logs so it should disambiguate the
situation. While you're mentioning this message, AFAICS it can already be
entirely wrong as-is, as it doesn't take into account the hba_file
configuration:

ALTER SYSTEM SET hba_file = '/tmp/myfile';

Hmm, indeed. And we load the GUCs before the HBA rules, obviously.
This could be made less confusing. Do you think that this can be
improved independently of the main patch, building the include
structure on top of it?

The set of HBA rules are very sensitive to their
ordering, and ReadDir() depends on readdir(), which provides a list
of files depending on its FS implementation. That does not seem like
a sane concept to me. I can this this order (tracked by rule_number)
being strictly enforced when it comes to the loading of files, though,
so I would recommend to focus on the implementation of "include"
rather than "include_dir".

But the same problem already exists for the postgresql.conf's include_dir.

Having an hba rule masking another isn't really better than having a GUC
value overloaded by another one. Usually people rely on a sane naming (like
01_file.conf and so on) to get a predictable behavior, and we even document
that very thoroughly for the postgresql.conf part. Improving the
documentation, as you already pointed out a bit above, should be enough?

Ah. ParseConfigDirectory() does a qsort() before processing each
file included in a folder. Unless I am missing something, your patch
reads the entries in the directory, but does not sort the files by
name to ensure a strict ordering of them.

While on it, it looks like you should have sanity checks similar to
ParseConfigDirectory() for CRLFs & friends, as of:
if (strspn(includedir, " \t\r\n") == strlen(includedir))

All this logic is very similar, so this could be perhaps refactored.

+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
This is a very important field.  I think that this explanation should
be extended and explain why relying on this number counts (aka this is
the order of the rules loaded across files).  Btw, this could be added
as a separate patch, even if this maps to the line number when it
comes to the ordering.

Agreed, I will improve the documentation to outline the importance of that
information.

Do you mean a separate patch to ease review or to eventually commit both
separately? FWIW I don't think that adding any form of inclusion in the hba
files should be done without a way for users to check the results. Any test
facility would also probably rely on this field.

Yeah, agreed that rule_number is important for the sake of the
inclusions. Still, I was wondering if rule_number makes sense on its
own, meaning that we could add it before working on the inclusion
logic. Having it without the inclusion is less interesting if you
have the line numbers to order the HBA rules, of course, as this is
enough to guess the priority of the HBA entries.

+                   /*
+                    * Only parse files with names ending in ".conf".
+                    * Explicitly reject files starting with ".". This
+                    * excludes things like "." and "..", as well as typical
+                    * hidden files, backup files, and editor debris.
+                    */
I don't think that there is any need to restrict that to files ending
with .conf.  We don't do that for postgresql.conf's include, for one.

I'm confused. Are you talking about postgresql.conf's include or include_dir
option? I'm pretty sure that I borrowed this logic from
src/backend/utils/misc/guc-file.l / ParseConfigDirectory(), which implements
the include_dir logic for the postgresql.conf file.

Ah, ParseConfigDirectory() does that for the sake of avoiding any
debris files. The reasoning (2a0c81a, aka
/messages/by-id/4EC341E1.1060908@2ndQuadrant.com)
relates to debris files like the ones created by editors and such. So
you are right to do so.

Finally I also added 0003, which is a POC for a new pg_hba_matches()
function, that can help DBA to understand why their configuration isn't
working as they expect. This only to start the discussion on that topic, the
code is for now really hackish, as I don't know how much this is wanted
and/or if some other behavior would be better, and there's also no
documentation or test. The function for now only takes an optional inet
(null means unix socket), the target role and an optional ssl flag and
returns the file, line and raw line matching if any, or null. For instance:

=# select * from pg_hba_matches('127.0.0.1'::inet, 'postgres');
-[ RECORD 1 ]-----------------------------------------------------------------
file_name | /tmp/pgbuild/toto.conf
line_num | 5
raw_line | host all all 127.0.0.1/32 trust

I will put more details and this example in the commit message if you want, but
if no one is interested in that feature I'm ok with discarding it.

Oh, OK, I was not really following, then. I'd be fine to revisit this
part if necessary.
--
Michael

#35Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#34)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, May 24, 2022 at 10:44:05AM +0900, Michael Paquier wrote:

On Mon, May 23, 2022 at 07:43:08PM +0800, Julien Rouhaud wrote:

That being said, I'd gladly drop that enum and only handle a single error
message, as the rest of the error context (including the owning file name and
line) should provide enough information to users.

If so, should I use "included authentication file" everywhere, or use something
else?

With the file name provided the context, "authentication file"?

So you mean having an error message like that (having an "include myconf"
in the HBA file):

LOG: could not open authentication file "myconf" as "/path/to/myconf": No such file or directory
LOG: pg_hba.conf was not reloaded

This would be somewhat consistent with how it's done for the postgresql.conf,
assuming that we do fix that hardcoded "pg_hba.conf":

LOG: could not open configuration file "/path/to/toto": No such file or directory
LOG: configuration file "/path/to/postgresql.conf" contains errors; no changes were applied

With this message it's clear that the file that couldn't be opened is an
included file.

However those two cases are slightly different, and technically the "secondary
file" is not an authentication file, just a list of tokens, so I'm still a bit
worried about ambiguity here.

The detail should be provided in the logs so it should disambiguate the
situation. While you're mentioning this message, AFAICS it can already be
entirely wrong as-is, as it doesn't take into account the hba_file
configuration:

ALTER SYSTEM SET hba_file = '/tmp/myfile';

Hmm, indeed. And we load the GUCs before the HBA rules, obviously.
This could be made less confusing. Do you think that this can be
improved independently of the main patch, building the include
structure on top of it?

Sure, I can split that in another commit.

After a bit more digging, I think that this comes from the fact that there's no
"official" name for this file. Even the documentation just says "the
pg_hba.conf file" [1]https://www.postgresql.org/docs/current/auth-pg-hba-conf.html. So using pg_hba.conf can either means explicitly
$PGDATA/pg_hba.conf or the instance's HBA file in general, whatever its
location.

I think it would be good to improve this, including in the doc, but I'm
assuming it's entirely for HEAD only, including the error messages?

If so, should I also change the doc to replace "pg_hba.conf" with something
else when it's not referring to the file default name?

I'm thinking of using "HBA file" to replace pg_hba.conf, and using
"authentication file" when it can be either the "HBA file" and the "User Name
Maps file", would that be ok?

But the same problem already exists for the postgresql.conf's include_dir.

Having an hba rule masking another isn't really better than having a GUC
value overloaded by another one. Usually people rely on a sane naming (like
01_file.conf and so on) to get a predictable behavior, and we even document
that very thoroughly for the postgresql.conf part. Improving the
documentation, as you already pointed out a bit above, should be enough?

Ah. ParseConfigDirectory() does a qsort() before processing each
file included in a folder. Unless I am missing something, your patch
reads the entries in the directory, but does not sort the files by
name to ensure a strict ordering of them.

While on it, it looks like you should have sanity checks similar to
ParseConfigDirectory() for CRLFs & friends, as of:
if (strspn(includedir, " \t\r\n") == strlen(includedir))

All this logic is very similar, so this could be perhaps refactored.

Indeed, I will move that in a common function to be consistent.

+      <para>
+       Rule number, in priority order, of this rule if the rule is valid,
+       otherwise null
+      </para></entry>
This is a very important field.  I think that this explanation should
be extended and explain why relying on this number counts (aka this is
the order of the rules loaded across files).  Btw, this could be added
as a separate patch, even if this maps to the line number when it
comes to the ordering.

Agreed, I will improve the documentation to outline the importance of that
information.

Do you mean a separate patch to ease review or to eventually commit both
separately? FWIW I don't think that adding any form of inclusion in the hba
files should be done without a way for users to check the results. Any test
facility would also probably rely on this field.

Yeah, agreed that rule_number is important for the sake of the
inclusions. Still, I was wondering if rule_number makes sense on its
own, meaning that we could add it before working on the inclusion
logic. Having it without the inclusion is less interesting if you
have the line numbers to order the HBA rules, of course, as this is
enough to guess the priority of the HBA entries.

Ah I see. Sure I could split it in another commit too, but this isn't
backpatchable (and pg15 isn't branched yet anyway) so I'm not sure how useful
that is. I will go head and do that anyway, it will be easy to merge the
commits if needed.

[1]: https://www.postgresql.org/docs/current/auth-pg-hba-conf.html

#36Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#35)
Re: Allow file inclusion in pg_hba and pg_ident files

On Thu, May 26, 2022 at 03:26:57PM +0800, Julien Rouhaud wrote:

So you mean having an error message like that (having an "include myconf"
in the HBA file):

LOG: could not open authentication file "myconf" as "/path/to/myconf": No such file or directory
LOG: pg_hba.conf was not reloaded

This would be somewhat consistent with how it's done for the postgresql.conf,
assuming that we do fix that hardcoded "pg_hba.conf":

LOG: could not open configuration file "/path/to/toto": No such file or directory
LOG: configuration file "/path/to/postgresql.conf" contains errors; no changes were applied

With this message it's clear that the file that couldn't be opened is an
included file.

Yes, consistency would be good here.

However those two cases are slightly different, and technically the "secondary
file" is not an authentication file, just a list of tokens, so I'm still a bit
worried about ambiguity here.

Hmm. Yes, using only this term could lead to some confusion.
Thinking more about that, perhaps you are right to use fully separated
terms for the ident and HBA parts, particularly once the inclusion is
in place as the file names could be anything, so we would lose context
about where a file for used for what.

After a bit more digging, I think that this comes from the fact that there's no
"official" name for this file. Even the documentation just says "the
pg_hba.conf file" [1]. So using pg_hba.conf can either means explicitly
$PGDATA/pg_hba.conf or the instance's HBA file in general, whatever its
location.

I think it would be good to improve this, including in the doc, but I'm
assuming it's entirely for HEAD only, including the error messages?

Yes, that would be a set of changes only for HEAD, once 16~ opens for
business. FWIW, the acronym "HBA" is defined as "Host-Based
Authentication", so we could use that as a base for the description of
the file, using simply HBA in the follow-up paragraphs for simplicity,
telling that pg_hba.conf is the default.

If so, should I also change the doc to replace "pg_hba.conf" with something
else when it's not referring to the file default name?

I'm thinking of using "HBA file" to replace pg_hba.conf, and using
"authentication file" when it can be either the "HBA file" and the "User Name
Maps file", would that be ok?

Using "HBA file" in the docs is fine by me, knowing that the acronym
is already defined. The modified parts of the docs should perhaps
mention once something like "Host-Based Authentication file (or HBA
file)" for clarity. For the error message, I think that we tend to
avoid those acronyms, don't we?

Ah I see. Sure I could split it in another commit too, but this isn't
backpatchable (and pg15 isn't branched yet anyway) so I'm not sure how useful
that is. I will go head and do that anyway, it will be easy to merge the
commits if needed.

The rule_number shines with the inclusion logic in place, still
splitting that into a separate commit makes the review and the commit
history cleaner IMO. Perhaps that's just me being overly-pedantic in
keeping a clean commit history :)
--
Michael

#37Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#36)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Thu, Jun 02, 2022 at 10:08:15AM +0900, Michael Paquier wrote:

On Thu, May 26, 2022 at 03:26:57PM +0800, Julien Rouhaud wrote:

After a bit more digging, I think that this comes from the fact that there's no
"official" name for this file. Even the documentation just says "the
pg_hba.conf file" [1]. So using pg_hba.conf can either means explicitly
$PGDATA/pg_hba.conf or the instance's HBA file in general, whatever its
location.

I think it would be good to improve this, including in the doc, but I'm
assuming it's entirely for HEAD only, including the error messages?

Yes, that would be a set of changes only for HEAD, once 16~ opens for
business. FWIW, the acronym "HBA" is defined as "Host-Based
Authentication", so we could use that as a base for the description of
the file, using simply HBA in the follow-up paragraphs for simplicity,
telling that pg_hba.conf is the default.

Ok.

If so, should I also change the doc to replace "pg_hba.conf" with something
else when it's not referring to the file default name?

I'm thinking of using "HBA file" to replace pg_hba.conf, and using
"authentication file" when it can be either the "HBA file" and the "User Name
Maps file", would that be ok?

Using "HBA file" in the docs is fine by me, knowing that the acronym
is already defined. The modified parts of the docs should perhaps
mention once something like "Host-Based Authentication file (or HBA
file)" for clarity. For the error message, I think that we tend to
avoid those acronyms, don't we?

I don't have an extensive knowledge of all the user facing error messages, but
after a quick grep I see multiple usage of OID, PID, GIN and other defined
acronyms. I also see multiple occurrences of "only heap AM is supported",
while AM isn't even a defined acronym.

It doesn't seem that my proposal would be inconsistent with existing messages
and will help to reduce the message length, but if you prefer to keep the full
name I'm fine with it. Those should be very rare and specialized errors
anyway.

While on the bikeshedding part, are you ok with the proposed keywords (include
and include_dir), behaving exactly like for postgresql.conf, and to also add
include_if_exists, so that we have the exact same possibilities with
postgresql.conf, pg_hba.conf and pg_ident.conf?

#38Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#37)
Re: Allow file inclusion in pg_hba and pg_ident files

On Fri, Jul 08, 2022 at 02:57:21PM +0800, Julien Rouhaud wrote:

My apologies for the late reply.

I don't have an extensive knowledge of all the user facing error messages, but
after a quick grep I see multiple usage of OID, PID, GIN and other defined
acronyms. I also see multiple occurrences of "only heap AM is supported",
while AM isn't even a defined acronym.

A lot depends on the context of the code, it seems.

It doesn't seem that my proposal would be inconsistent with existing messages
and will help to reduce the message length, but if you prefer to keep the full
name I'm fine with it. Those should be very rare and specialized errors
anyway.

So you mean to use "HBA file" instead of pg_hba.conf and
"authentication file" when it can be either one of an HBA file or a
mapping file? That would be okay by me. We would have a full cycle
to tune them depending on the feedback we'd get afterwards.

While on the bikeshedding part, are you ok with the proposed keywords (include
and include_dir), behaving exactly like for postgresql.conf, and to also add
include_if_exists, so that we have the exact same possibilities with
postgresql.conf, pg_hba.conf and pg_ident.conf?

Okay, agreed for consistency. With include_dir being careful about
the ordering of the entries and ignoring anything else than a .conf
file (that's something you mentioned already upthread).
--
Michael

#39Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#38)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Mon, Jul 11, 2022 at 10:16:44AM +0900, Michael Paquier wrote:

On Fri, Jul 08, 2022 at 02:57:21PM +0800, Julien Rouhaud wrote:

My apologies for the late reply.

I don't have an extensive knowledge of all the user facing error messages, but
after a quick grep I see multiple usage of OID, PID, GIN and other defined
acronyms. I also see multiple occurrences of "only heap AM is supported",
while AM isn't even a defined acronym.

A lot depends on the context of the code, it seems.

It doesn't seem that my proposal would be inconsistent with existing messages
and will help to reduce the message length, but if you prefer to keep the full
name I'm fine with it. Those should be very rare and specialized errors
anyway.

So you mean to use "HBA file" instead of pg_hba.conf and
"authentication file" when it can be either one of an HBA file or a
mapping file? That would be okay by me.

Yes, it seems to me like a good compromise for not being overly verbose and
still being understandable.

We would have a full cycle
to tune them depending on the feedback we'd get afterwards.

Agreed.

While on the bikeshedding part, are you ok with the proposed keywords (include
and include_dir), behaving exactly like for postgresql.conf, and to also add
include_if_exists, so that we have the exact same possibilities with
postgresql.conf, pg_hba.conf and pg_ident.conf?

Okay, agreed for consistency. With include_dir being careful about
the ordering of the entries and ignoring anything else than a .conf
file (that's something you mentioned already upthread).

Ok! All those that should be covered by new regression test so it should be
clear what is being implemented.

While on the regression tests topic, I started to implement those and faced
some problems quite fast when trying to workaround the problem we previously
discussed (1).

So first, even if we can test 99% of the features with just testing the views
output, I think it's should use the TAP framework since the tests will have to
mess with the pg_ident/pg_hba files. It's way easier to modify the auth files,
and it uses a dedicated instance so we don't have to worry about breaking other
test that would run concurrently.

Also, if we want to test the views error reporting we have to use a persistent
connection (as in interactive_psql()), otherwise tests will immediately fail on
Windows / EXEC_BACKEND builds. Adding the ability to run queries and wait for
completion on top of interactive_psql() doesn't seem to cause any problem, but
interpreting the results does.

Since it's just manipulating the psql's stdin/stdout, we retrieve the prompt
and executed query too. So for instance, if you just want to test "SELECT 1",
you will have to cleanup something like

dbname=# SELECT 1;
1
dbname#

That may still be workable by splitting the output per newline (and possibly
removing the first prompt before sending the query text), and remove the first
and last entry (assuming you want to test somewhat sane data, and not e.g. run
the regression test on a database containing a newline), but then you have to
also account for possible escape sequences, for instance if you use
enable-bracketed-paste. In that case, the output becomes something like

dbname=# SELECT 1;
[?2004l
1
[?2004hpostgres=#

It could probably be handled with some regexp to remove escape sequences and
remove empty lines, but it seems like really fragile, and thus a very bad idea.

I'm not really sure what should be done here. The best compromise I can think
of is to split the tests in 3 parts:

1) view reporting with various inclusions using safe_psql()
2) log error reporting
3) view reporting with various inclusions errors, using safe_psql()

And when testing 3), detect first if we can still connect after introducing
errors. If not, assume this is Windows / EXEC_BACKEND and give up here without
reporting an error. Otherwise continue, and fail the test if we later can't
connect anymore. As discussed previously, detecting that the build is using
the fork emulation code path doesn't seem worthwhile so guessing it from the
psql error may be a better approach.

Do you have any better idea, or do you have comments on this approach?

[1]: /messages/by-id/YkFhpydhyeNNo3Xl@paquier.xyz

#40Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#39)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Jul 18, 2022 at 03:11:51PM +0800, Julien Rouhaud wrote:

So first, even if we can test 99% of the features with just testing the views
output, I think it's should use the TAP framework since the tests will have to
mess with the pg_ident/pg_hba files. It's way easier to modify the auth files,
and it uses a dedicated instance so we don't have to worry about breaking other
test that would run concurrently.

Agreed.

That may still be workable by splitting the output per newline (and possibly
removing the first prompt before sending the query text), and remove the first
and last entry (assuming you want to test somewhat sane data, and not e.g. run
the regression test on a database containing a newline), but then you have to
also account for possible escape sequences, for instance if you use
enable-bracketed-paste. In that case, the output becomes something like

dbname=# SELECT 1;
[?2004l
1
[?2004hpostgres=#

It could probably be handled with some regexp to remove escape sequences and
remove empty lines, but it seems like really fragile, and thus a very bad idea.

Hmm. Indeed, that sounds fragile. And I don't really think that we
should make more testing infrastructure a requirement for this patch
as being able to maintain a connection while running the tests is
something we've bumped on for a bit of time.

I'm not really sure what should be done here. The best compromise I can think
of is to split the tests in 3 parts:

1) view reporting with various inclusions using safe_psql()

You mean in the case where the HBA and indent files can be loaded,
so as it is possible to peek at the system views without the
EXEC_BACKEND problem, right?

2) log error reporting

This one should be reliable and stable enough by parsing the logs of
the backend, thanks to connect_ok() and connect_fails().

3) view reporting with various inclusions errors, using safe_psql()

And when testing 3), detect first if we can still connect after introducing
errors. If not, assume this is Windows / EXEC_BACKEND and give up here without
reporting an error. Otherwise continue, and fail the test if we later can't
connect anymore. As discussed previously, detecting that the build is using
the fork emulation code path doesn't seem worthwhile so guessing it from the
psql error may be a better approach.

Yeah, we could do that. Now we may also fail on other patterns, so we
would need to make sure that a set of expected error outputs are the
ones generated? I'd be fine to give up testing the error output
generated in the system views at the end. Without a persistent
connection state with the same kind of APIs as any of the drivers able
to do so, that's going to be a PITA to maintain.
--
Michael

#41Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#25)
1 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Mon, Mar 28, 2022 at 04:22:32PM +0900, Michael Paquier wrote:

On Mon, Mar 28, 2022 at 04:20:07PM +0900, Michael Paquier wrote:

See the attached, for reference, but it would fail with EXEC_BACKEND
on WIN32.

Ditto.

While working on the full regression test coverage for the file inclusion
thing, I discovered an embarrassing typo in the pg_ident_file_mapping
infrastructure, which was using the hba file name rather than the ident file
name in one of the calls.

It doesn't have much impact most of the time. The filename is reported if
there's an IO error while reading the already opened correct file. The real
problem is if the hba_file and ident_file are stored in different directory,
any secondary file (@filename) in the pg_ident.conf would be searched in the
wrong directory. With the pending file inclusion patchset, the problem is
immediately visible as the view is reporting the wrong file name.

Simple fix attached. I'll add a v15 open item shortly.

Attachments:

v1-0001-Fix-fill_ident_view-incorrect-usage-of-HbaFileNam.patchtext/plain; charset=us-asciiDownload
From d36010e7fec78b3ea25255767b8a2478f85fd325 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 26 Jul 2022 12:52:35 +0800
Subject: [PATCH v1] Fix fill_ident_view incorrect usage of HbaFileName

Thinko introduced in a2c84990bea.

Patchpatch to 15, as the original commit.

Author: Julien Rouhaud
---
 src/backend/utils/adt/hbafuncs.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 598259718c..9e5794071c 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -512,7 +512,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open usermap file \"%s\": %m",
 						IdentFileName)));
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
-- 
2.37.0

#42Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#41)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Jul 26, 2022 at 01:04:02PM +0800, Julien Rouhaud wrote:

It doesn't have much impact most of the time. The filename is reported if
there's an IO error while reading the already opened correct file. The real
problem is if the hba_file and ident_file are stored in different directory,
any secondary file (@filename) in the pg_ident.conf would be searched in the
wrong directory. With the pending file inclusion patchset, the problem is
immediately visible as the view is reporting the wrong file name.

Oops, obviously. I'll go and fix that as that's on me.

Simple fix attached. I'll add a v15 open item shortly.

Thanks. Better not to lose track of it.
--
Michael

#43Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#40)
6 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Tue, Jul 19, 2022 at 03:13:12PM +0900, Michael Paquier wrote:

On Mon, Jul 18, 2022 at 03:11:51PM +0800, Julien Rouhaud wrote:

I'm not really sure what should be done here. The best compromise I can think
of is to split the tests in 3 parts:

1) view reporting with various inclusions using safe_psql()

You mean in the case where the HBA and indent files can be loaded,
so as it is possible to peek at the system views without the
EXEC_BACKEND problem, right?

Yes, testing the general behavior when there are no errors in the auth files.

2) log error reporting

This one should be reliable and stable enough by parsing the logs of
the backend, thanks to connect_ok() and connect_fails().

I meant testing the postgres logs reporting, something like generating entirely
bogus files, restarting, checking that the start failed and verify that the
logs are expected. With a variation that if only the pg_ident.conf is bogus,
the server will (and should) still start, so both needs to be tested
separately.

3) view reporting with various inclusions errors, using safe_psql()

And when testing 3), detect first if we can still connect after introducing
errors. If not, assume this is Windows / EXEC_BACKEND and give up here without
reporting an error. Otherwise continue, and fail the test if we later can't
connect anymore. As discussed previously, detecting that the build is using
the fork emulation code path doesn't seem worthwhile so guessing it from the
psql error may be a better approach.

Yeah, we could do that. Now we may also fail on other patterns, so we
would need to make sure that a set of expected error outputs are the
ones generated? I'd be fine to give up testing the error output
generated in the system views at the end. Without a persistent
connection state with the same kind of APIs as any of the drivers able
to do so, that's going to be a PITA to maintain.

Yes, or just checking that the error is expected on the psql side as the server
will report it.

I've been working on all of that and came up with the attached v8.

- 0001: I modified things as discussed previously to report the real auth file
names rather than the hardcoded "pg_ident.conf" and "pg_hba.conf" in the
various log messages
- 0002 and 0003 are minor fixes to error logs more consistent
- 0004: the rule_number / mapping_number addition in the views in a separate
commit
- 0005: the main file inclusion patch. Only a few minor bugfix since
previous version discovered thanks to the tests (a bit more about it after),
and documentation tweaks based on previous discussions
- 0006: the pg_hba_matches() POC, no changes

About the regression tests:
I added a new 003_file_inclusion.pl in src/test/authentication TAP tests to do
things as previously described, ie test both extisting behavior and new
features added, for working and non working scenarios. The view error
reporting are bypassed for Windows / EXEC_BACKEND build by detecting a
connection failure with the expected error reported by the server. If the
error doesn't match, the rest of the tests are still skipped but overall test
will fail due to the mismatch in the reported error.

It's a bit troublesome to write such tests, as you need to keep in sync the
various rule and line number, format things differently depending on whether
you want to check the logs or the view with each new tests added and so on.

To make the tests easier to write (and to maintain) I added some wrapper
functions to add lines in the wanted files that allow to automatically
generates the wanted regex (for the logs) or output (for the view). There are
still things to be careful about and a bit of duplication, but this way I can
easily modify any test, or add new ones, without the need to modify everything
around. As written, it also uses the same included files for the log error
reporting and the view error reporting, so there's no need to define everything
twice.

The tests work as expected locally on a normal build and an EXEC_BACKEND build,
and also on the CI.

Attachments:

v8-0001-Use-real-file-names-rather-than-pg_hba.conf-pg_id.patchtext/plain; charset=us-asciiDownload
From 7232b708099eec0fd27a43ed722388f4988655da Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 3 Jun 2022 15:56:23 +0800
Subject: [PATCH v8 1/6] Use real file names rather than
 pg_hba.conf/pg_ident.conf in messages.

---
 src/backend/postmaster/postmaster.c | 7 ++++---
 src/backend/utils/init/postinit.c   | 3 ++-
 2 files changed, 6 insertions(+), 4 deletions(-)

diff --git a/src/backend/postmaster/postmaster.c b/src/backend/postmaster/postmaster.c
index e541b16bdb..8bcfedef64 100644
--- a/src/backend/postmaster/postmaster.c
+++ b/src/backend/postmaster/postmaster.c
@@ -1419,7 +1419,8 @@ PostmasterMain(int argc, char *argv[])
 		 * since there is no way to connect to the database in this case.
 		 */
 		ereport(FATAL,
-				(errmsg("could not load pg_hba.conf")));
+			/* translator: %s is a configuration file */
+				(errmsg("could not load %s", HbaFileName)));
 	}
 	if (!load_ident())
 	{
@@ -2769,11 +2770,11 @@ SIGHUP_handler(SIGNAL_ARGS)
 		if (!load_hba())
 			ereport(LOG,
 			/* translator: %s is a configuration file */
-					(errmsg("%s was not reloaded", "pg_hba.conf")));
+					(errmsg("%s was not reloaded", HbaFileName)));
 
 		if (!load_ident())
 			ereport(LOG,
-					(errmsg("%s was not reloaded", "pg_ident.conf")));
+					(errmsg("%s was not reloaded", IdentFileName)));
 
 #ifdef USE_SSL
 		/* Reload SSL configuration as well */
diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 29f70accb2..8801a5f5f5 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -217,7 +217,8 @@ PerformAuthentication(Port *port)
 		 * since there is no way to connect to the database in this case.
 		 */
 		ereport(FATAL,
-				(errmsg("could not load pg_hba.conf")));
+			/* translator: %s is a configuration file */
+				(errmsg("could not load %s", HbaFileName)));
 	}
 
 	if (!load_ident())
-- 
2.37.0

v8-0002-Add-file-name-file-line-context-for-incorrect-reg.patchtext/plain; charset=us-asciiDownload
From 45c22e6186ded15605d07fc4c8abc38a1f9f2852 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 29 Jul 2022 14:47:09 +0800
Subject: [PATCH v8 2/6] Add file name / file line context for incorrect regex
 in ident files.

For consistency with all other error messages report those information, which
are indeed useful to debug configuration errors.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/libpq/hba.c | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 327a4b42af..1ad09f7dc6 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -2372,7 +2372,9 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 			ereport(elevel,
 					(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
 					 errmsg("invalid regular expression \"%s\": %s",
-							parsedline->ident_user + 1, errstr)));
+							parsedline->ident_user + 1, errstr),
+					 errcontext("line %d of configuration file \"%s\"",
+							line_num, IdentFileName)));
 
 			*err_msg = psprintf("invalid regular expression \"%s\": %s",
 								parsedline->ident_user + 1, errstr);
-- 
2.37.0

v8-0003-Standardize-IDENT_FIELD_ABSENT-error-messages.patchtext/plain; charset=us-asciiDownload
From 775098bfffaef9f3f380b64c94c94479e9c2513c Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Fri, 29 Jul 2022 17:21:49 +0800
Subject: [PATCH v8 3/6] Standardize IDENT_FIELD_ABSENT error messages

The rest of the code emit the file name and line in a distinct errcontext so
make IDENT_FIELD_ABSENT consistent.  This will help an upcoming commit will add
regression tests for variour error scenario.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/libpq/hba.c | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 1ad09f7dc6..deee05c197 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -900,8 +900,9 @@ do { \
 	if (!field) { \
 		ereport(elevel, \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
-				 errmsg("missing entry in file \"%s\" at end of line %d", \
-						IdentFileName, line_num))); \
+				 errmsg("missing entry at end of line"), \
+				 errcontext("line %d of configuration file \"%s\"", \
+							line_num, IdentFileName))); \
 		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
-- 
2.37.0

v8-0004-Add-rule_number-mapping_number-to-the-pg_hba-pg_i.patchtext/plain; charset=us-asciiDownload
From dba686a27746a3235d97b2c11439f1c13166deda Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 10:59:51 +0800
Subject: [PATCH v8 4/6] Add rule_number / mapping_number to the
 pg_hba/pg_ident views.

Author: Julien Rouhaud
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/system-views.sgml      | 22 +++++++++++++
 src/backend/utils/adt/hbafuncs.c    | 50 ++++++++++++++++++++++-------
 src/include/catalog/pg_proc.dat     | 11 ++++---
 src/test/regress/expected/rules.out | 10 +++---
 4 files changed, 72 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 26ce83eb9b..b4ee40c042 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -991,6 +991,18 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number of this rule among all rules if the rule is valid, otherwise
+       null. This indicates the order in which each rule will be considered
+       until the first matching one, if any, is used to perform authentication
+       with the client.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
@@ -1131,6 +1143,16 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Mapping number, in priority order, of this mapping if the mapping is
+       valid, otherwise null
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 9e5794071c..c9be4bff1f 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,10 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, int lineno, HbaLine *hba,
+						  const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int lineno, IdentLine *ident, const char *err_msg);
+							int mapping_number, int lineno, IdentLine *ident,
+							const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 10
 
 /*
  * fill_hba_line
@@ -165,6 +167,7 @@ get_hba_options(HbaLine *hba)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * rule_number: unique rule identifier among all valid rules
  * lineno: pg_hba.conf line number (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -174,7 +177,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +197,11 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +345,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +368,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,7 +403,11 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
+		/* No error, set a new rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
 					  hbaline, tok_line->err_msg);
 	}
 
@@ -430,8 +444,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -439,6 +453,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * mapping_number: unique rule identifier among all valid rules
  * lineno: pg_ident.conf line number (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, int lineno, IdentLine *ident,
+				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,11 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +494,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +516,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,8 +551,12 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-						tok_line->err_msg);
+		/* No error, set a new mapping number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->line_num, identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2e41f4d9e8..e544f9f758 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6128,15 +6128,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o}',
+  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7ec3d2688f..79408710e0 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,8 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1347,14 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v8-0005-Allow-file-inclusion-in-pg_hba-and-pg_ident-files.patchtext/plain; charset=us-asciiDownload
From b47d5fd1ecbf32baba12a07d061c062c6585fe14 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 11:15:06 +0800
Subject: [PATCH v8 5/6] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  22 +-
 src/backend/libpq/hba.c                       | 485 ++++++++++---
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  43 +-
 src/backend/utils/misc/guc-file.l             | 250 +++----
 src/include/catalog/pg_proc.dat               |  12 +-
 src/include/libpq/hba.h                       |   5 +-
 src/include/utils/guc.h                       |   2 +
 .../authentication/t/003_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 12 files changed, 1325 insertions(+), 283 deletions(-)
 create mode 100644 src/test/authentication/t/003_file_inclusion.pl

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 433759928b..e4eacab4a5 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -837,8 +888,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -849,6 +902,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index b4ee40c042..35ed69a64a 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1003,12 +1003,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1153,12 +1162,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this mapping
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_ident.conf</filename>
+       Line number of this mapping in the given <literal>file_name</literal>
       </para></entry>
      </row>
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index deee05c197..814bfcf30d 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -21,6 +21,7 @@
 #include <fcntl.h>
 #include <sys/param.h>
 #include <sys/socket.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -68,6 +69,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -112,10 +119,22 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int depth, int elevel,
+							   char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   bool strict, const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -302,7 +321,7 @@ copy_auth_token(AuthToken *in)
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int depth, int elevel, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -318,7 +337,7 @@ next_field_expand(const char *filename, char **lineptr,
 
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
-			tokens = tokenize_inc_file(tokens, filename, buf + 1,
+			tokens = tokenize_inc_file(tokens, filename, buf + 1, depth + 1,
 									   elevel, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
@@ -346,6 +365,7 @@ static List *
 tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
+				  int depth,
 				  int elevel,
 				  char **err_msg)
 {
@@ -355,39 +375,30 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
 	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
+		*err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", *err_msg)));
+		return tokens;
 	}
 
-	inc_file = AllocateFile(inc_fullname, "r");
-	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, true,
+							 outer_filename, elevel, err_msg, &inc_fullname);
 
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
+	if (inc_file == NULL)
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, depth + 1,
+								 elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -425,11 +436,38 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -438,30 +476,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int depth, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -514,7 +544,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		{
 			List	   *current_field;
 
-			current_field = next_field_expand(filename, &lineptr,
+			current_field = next_field_expand(filename, &lineptr, depth,
 											  elevel, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
@@ -522,29 +552,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetDirConfFiles(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -859,7 +987,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -879,7 +1007,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -902,7 +1030,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry at end of line"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -915,7 +1043,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -938,6 +1066,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -952,6 +1081,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -966,7 +1096,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for connection type"),
 				 errhint("Specify exactly one connection type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for connection type";
 		return NULL;
 	}
@@ -980,7 +1110,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("local connections are not supported by this build"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "local connections are not supported by this build";
 		return NULL;
 #endif
@@ -1004,7 +1134,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("hostssl record cannot match because SSL is disabled"),
 						 errhint("Set ssl = on in postgresql.conf."),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "hostssl record cannot match because SSL is disabled";
 			}
 #else
@@ -1012,7 +1142,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostssl record cannot match because SSL is not supported by this build";
 #endif
 		}
@@ -1024,7 +1154,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
 #endif
 		}
@@ -1045,7 +1175,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid connection type \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid connection type \"%s\"", token->string);
 		return NULL;
 	}
@@ -1058,7 +1188,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before database specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before database specification";
 		return NULL;
 	}
@@ -1078,7 +1208,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before role specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before role specification";
 		return NULL;
 	}
@@ -1100,7 +1230,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("end-of-line before IP address specification"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "end-of-line before IP address specification";
 			return NULL;
 		}
@@ -1112,7 +1242,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("multiple values specified for host address"),
 					 errhint("Specify one address range per line."),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "multiple values specified for host address";
 			return NULL;
 		}
@@ -1171,7 +1301,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("invalid IP address \"%s\": %s",
 								str, gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("invalid IP address \"%s\": %s",
 									str, gai_strerror(ret));
 				if (gai_result)
@@ -1191,7 +1321,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
 										token->string);
 					return NULL;
@@ -1205,7 +1335,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid CIDR mask in address \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid CIDR mask in address \"%s\"",
 										token->string);
 					return NULL;
@@ -1225,7 +1355,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("end-of-line before netmask specification"),
 							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "end-of-line before netmask specification";
 					return NULL;
 				}
@@ -1236,7 +1366,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("multiple values specified for netmask"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "multiple values specified for netmask";
 					return NULL;
 				}
@@ -1251,7 +1381,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid IP mask \"%s\": %s",
 									token->string, gai_strerror(ret)),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid IP mask \"%s\": %s",
 										token->string, gai_strerror(ret));
 					if (gai_result)
@@ -1270,7 +1400,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("IP address and mask do not match"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "IP address and mask do not match";
 					return NULL;
 				}
@@ -1286,7 +1416,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before authentication method"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before authentication method";
 		return NULL;
 	}
@@ -1298,7 +1428,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for authentication type"),
 				 errhint("Specify exactly one authentication type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for authentication type";
 		return NULL;
 	}
@@ -1335,7 +1465,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
 			return NULL;
 		}
@@ -1376,7 +1506,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\"",
 							token->string);
 		return NULL;
@@ -1389,7 +1519,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\": not supported by this build",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
 							token->string);
 		return NULL;
@@ -1411,7 +1541,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("gssapi authentication is not supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "gssapi authentication is not supported on local sockets";
 		return NULL;
 	}
@@ -1423,7 +1553,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("peer authentication is only supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "peer authentication is only supported on local sockets";
 		return NULL;
 	}
@@ -1441,7 +1571,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("cert authentication is only supported on hostssl connections"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "cert authentication is only supported on hostssl connections";
 		return NULL;
 	}
@@ -1491,7 +1621,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("authentication option not in name=value format: %s", token->string),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("authentication option not in name=value format: %s",
 									token->string);
 				return NULL;
@@ -1535,7 +1665,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
 				return NULL;
 			}
@@ -1546,7 +1676,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(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)));
+								line_num, file_name)));
 			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
 			return NULL;
 		}
@@ -1562,7 +1692,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
 			return NULL;
 		}
@@ -1579,7 +1709,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS servers cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS servers cannot be empty";
 			return NULL;
 		}
@@ -1590,7 +1720,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS secrets cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS secrets cannot be empty";
 			return NULL;
 		}
@@ -1609,7 +1739,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiussecrets),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiussecrets),
 								list_length(parsedline->radiusservers));
@@ -1625,7 +1755,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusports),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusports),
 								list_length(parsedline->radiusservers));
@@ -1641,7 +1771,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusidentifiers),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusidentifiers),
 								list_length(parsedline->radiusservers));
@@ -1676,6 +1806,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -1699,7 +1830,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientcert can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1716,7 +1847,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
 				return false;
 			}
@@ -1729,7 +1860,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientcert: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1741,7 +1872,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientname can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientname can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1760,7 +1891,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientname: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1846,7 +1977,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid ldapscheme value: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 		hbaline->ldapscheme = pstrdup(val);
 	}
 	else if (strcmp(name, "ldapserver") == 0)
@@ -1864,7 +1995,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid LDAP port number: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid LDAP port number: \"%s\"", val);
 			return false;
 		}
@@ -1958,7 +2089,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS server list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -1977,7 +2108,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
 								(char *) lfirst(l), gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				if (gai_result)
 					pg_freeaddrinfo_all(hints.ai_family, gai_result);
 
@@ -2006,7 +2137,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS port list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
 			return false;
 		}
@@ -2019,7 +2150,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("invalid RADIUS port number: \"%s\"", val),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 
 				return false;
 			}
@@ -2042,7 +2173,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS secret list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2064,7 +2195,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS identifiers list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2078,7 +2209,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				 errmsg("unrecognized authentication option name: \"%s\"",
 						name),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
 							name);
 		return false;
@@ -2226,7 +2357,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2297,6 +2428,137 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename, bool strict,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		if (strict)
+		{
+			switch (kind)
+			{
+				case SecondaryAuthFile:
+					msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+					msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+					break;
+				case IncludedAuthFile:
+					msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+					msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+					break;
+				default:
+					elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+					break;
+			}
+
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg(msglog, inc_filename, *inc_fullname)));
+			*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+								strerror(save_errno));
+		}
+		else
+		{
+			Assert(kind == IncludedAuthFile);
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							*inc_fullname)));
+		}
+
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		char *err_msg;
+
+		err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", err_msg)));
+		return err_msg;
+	}
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, strict,
+							 outer_filename, elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_inc_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+			return NULL;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, depth, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2315,6 +2577,7 @@ load_hba(void)
 IdentLine *
 parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
+	char	   *file_name = tok_line->file_name;
 	int			line_num = tok_line->line_num;
 	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
@@ -2375,7 +2638,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("invalid regular expression \"%s\": %s",
 							parsedline->ident_user + 1, errstr),
 					 errcontext("line %d of configuration file \"%s\"",
-							line_num, IdentFileName)));
+							line_num, file_name)));
 
 			*err_msg = psprintf("invalid regular expression \"%s\": %s",
 								parsedline->ident_user + 1, errstr);
@@ -2610,7 +2873,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index c9be4bff1f..15326a01e2 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int mapping_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int mapping_number, const char *filename,
+							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique rule identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -202,6 +203,8 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -345,7 +348,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -386,7 +389,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open configuration file \"%s\": %m",
 						HbaFileName)));
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -407,8 +410,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			rule_number++;
 
-		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -445,7 +448,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -454,7 +457,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * mapping_number: unique rule identifier among all valid rules
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -463,8 +467,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int mapping_number, int lineno, IdentLine *ident,
-				const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -482,6 +486,8 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -494,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -534,7 +540,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open usermap file \"%s\": %m",
 						IdentFileName)));
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -556,7 +562,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			mapping_number++;
 
 		fill_ident_line(tuple_store, tupdesc, mapping_number,
-						tok_line->line_num, identline, tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index ce5633844c..b3e18e48cf 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -700,6 +700,122 @@ GUC_flex_fatal(const char *msg)
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * Returns the list of config files located in a directory, in alphabetical
+ * order.
+ *
+ * We don't check for recursion or too-deep nesting depth here, its up to the
+ * caller to take care of that.
+ */
+char **
+GetDirConfFiles(const char *includedir, const char *calling_file, int elevel,
+				int *num_filenames, char **err_msg)
+{
+	char	   *directory;
+	DIR		   *d;
+	struct dirent *de;
+	char	  **filenames;
+	int			size_filenames;
+
+	/*
+	 * Reject directory name that is all-blank (including empty), as that
+	 * leads to confusion --- we'd read the containing directory, typically
+	 * resulting in recursive inclusion of the same file(s).
+	 */
+	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty configuration directory name: \"%s\"",
+						includedir)));
+		*err_msg = "empty configuration directory name";
+		return NULL;
+	}
+
+	directory = AbsoluteConfigLocation(includedir, calling_file);
+	d = AllocateDir(directory);
+	if (d == NULL)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration directory \"%s\": %m",
+						directory)));
+		*err_msg = psprintf("could not open directory \"%s\"", directory);
+		filenames = NULL;
+		goto cleanup;
+	}
+
+	/*
+	 * Read the directory and put the filenames in an array, so we can sort
+	 * them prior to caller processing the contents.
+	 */
+	size_filenames = 32;
+	filenames = (char **) palloc(size_filenames * sizeof(char *));
+	*num_filenames = 0;
+
+	while ((de = ReadDir(d, directory)) != NULL)
+	{
+		struct stat st;
+		char		filename[MAXPGPATH];
+
+		/*
+		 * Only parse files with names ending in ".conf".  Explicitly reject
+		 * files starting with ".".  This excludes things like "." and "..",
+		 * as well as typical hidden files, backup files, and editor debris.
+		 */
+		if (strlen(de->d_name) < 6)
+			continue;
+		if (de->d_name[0] == '.')
+			continue;
+		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+			continue;
+
+		join_path_components(filename, directory, de->d_name);
+		canonicalize_path(filename);
+		if (stat(filename, &st) == 0)
+		{
+			/* Ignore directories. */
+			if (S_ISDIR(st.st_mode))
+				continue;
+
+			/* Add file to array, increasing its size in blocks of 32 */
+			if (*num_filenames >= size_filenames)
+			{
+				size_filenames += 32;
+				filenames = (char **) repalloc(filenames,
+										size_filenames * sizeof(char *));
+			}
+			filenames[*num_filenames] = pstrdup(filename);
+			(*num_filenames)++;
+		}
+		else
+		{
+			/*
+			 * stat does not care about permissions, so the most likely reason
+			 * a file can't be accessed now is if it was removed between the
+			 * directory listing and now.
+			 */
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg("could not stat file \"%s\": %m",
+							filename)));
+			*err_msg = psprintf("could not stat file \"%s\"", filename);
+			pfree(filenames);
+			filenames = NULL;
+			goto cleanup;
+		}
+	}
+
+	if (*num_filenames > 0)
+		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
+
+cleanup:
+	if (d)
+		FreeDir(d);
+	pfree(directory);
+	return filenames;
+}
+
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -961,138 +1077,32 @@ ParseConfigDirectory(const char *includedir,
 					 ConfigVariable **head_p,
 					 ConfigVariable **tail_p)
 {
-	char	   *directory;
-	DIR		   *d;
-	struct dirent *de;
+	char	   *err_msg;
 	char	  **filenames;
 	int			num_filenames;
-	int			size_filenames;
-	bool		status;
 
-	/*
-	 * Reject directory name that is all-blank (including empty), as that
-	 * leads to confusion --- we'd read the containing directory, typically
-	 * resulting in recursive inclusion of the same file(s).
-	 */
-	if (strspn(includedir, " \t\r\n") == strlen(includedir))
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("empty configuration directory name: \"%s\"",
-						includedir)));
-		record_config_file_error("empty configuration directory name",
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		return false;
-	}
+	filenames = GetDirConfFiles(includedir, calling_file, elevel,
+							   &num_filenames, &err_msg);
 
-	/*
-	 * We don't check for recursion or too-deep nesting depth here; the
-	 * subsequent calls to ParseConfigFile will take care of that.
-	 */
-
-	directory = AbsoluteConfigLocation(includedir, calling_file);
-	d = AllocateDir(directory);
-	if (d == NULL)
+	if (!filenames)
 	{
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration directory \"%s\": %m",
-						directory)));
-		record_config_file_error(psprintf("could not open directory \"%s\"",
-										  directory),
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		status = false;
-		goto cleanup;
-	}
-
-	/*
-	 * Read the directory and put the filenames in an array, so we can sort
-	 * them prior to processing the contents.
-	 */
-	size_filenames = 32;
-	filenames = (char **) palloc(size_filenames * sizeof(char *));
-	num_filenames = 0;
-
-	while ((de = ReadDir(d, directory)) != NULL)
-	{
-		struct stat st;
-		char		filename[MAXPGPATH];
-
-		/*
-		 * Only parse files with names ending in ".conf".  Explicitly reject
-		 * files starting with ".".  This excludes things like "." and "..",
-		 * as well as typical hidden files, backup files, and editor debris.
-		 */
-		if (strlen(de->d_name) < 6)
-			continue;
-		if (de->d_name[0] == '.')
-			continue;
-		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
-			continue;
-
-		join_path_components(filename, directory, de->d_name);
-		canonicalize_path(filename);
-		if (stat(filename, &st) == 0)
-		{
-			if (!S_ISDIR(st.st_mode))
-			{
-				/* Add file to array, increasing its size in blocks of 32 */
-				if (num_filenames >= size_filenames)
-				{
-					size_filenames += 32;
-					filenames = (char **) repalloc(filenames,
-											size_filenames * sizeof(char *));
-				}
-				filenames[num_filenames] = pstrdup(filename);
-				num_filenames++;
-			}
-		}
-		else
-		{
-			/*
-			 * stat does not care about permissions, so the most likely reason
-			 * a file can't be accessed now is if it was removed between the
-			 * directory listing and now.
-			 */
-			ereport(elevel,
-					(errcode_for_file_access(),
-					 errmsg("could not stat file \"%s\": %m",
-							filename)));
-			record_config_file_error(psprintf("could not stat file \"%s\"",
-											  filename),
-									 calling_file, calling_lineno,
-									 head_p, tail_p);
-			status = false;
-			goto cleanup;
-		}
+		record_config_file_error(err_msg, calling_file, calling_lineno, head_p,
+								 tail_p);
+		return false;
 	}
 
-	if (num_filenames > 0)
+	for (int i = 0; i < num_filenames; i++)
 	{
-		int			i;
-
-		qsort(filenames, num_filenames, sizeof(char *), pg_qsort_strcmp);
-		for (i = 0; i < num_filenames; i++)
+		if (!ParseConfigFile(filenames[i], true,
+							 calling_file, calling_lineno,
+							 depth, elevel,
+							 head_p, tail_p))
 		{
-			if (!ParseConfigFile(filenames[i], true,
-								 calling_file, calling_lineno,
-								 depth, elevel,
-								 head_p, tail_p))
-			{
-				status = false;
-				goto cleanup;
-			}
+			return false;
 		}
 	}
-	status = true;
 
-cleanup:
-	if (d)
-		FreeDir(d);
-	pfree(directory);
-	return status;
+	return true;
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e544f9f758..d66b2443a4 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6128,16 +6128,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 90036f7bcd..0ea100d1b8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -155,6 +156,7 @@ typedef struct AuthToken
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
@@ -174,6 +176,7 @@ extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int depth,
+										int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index e734493a48..1a3ab6306d 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -145,6 +145,8 @@ typedef struct ConfigVariable
 	struct ConfigVariable *next;
 } ConfigVariable;
 
+extern char **GetDirConfFiles(const char *includedir, const char *calling_file,
+							  int elevel, int *num_filenames, char **err_msg);
 extern bool ParseConfigFile(const char *config_file, bool strict,
 							const char *calling_file, int calling_lineno,
 							int depth, int elevel,
diff --git a/src/test/authentication/t/003_file_inclusion.pl b/src/test/authentication/t/003_file_inclusion.pl
new file mode 100644
index 0000000000..8eae72b8d4
--- /dev/null
+++ b/src/test/authentication/t/003_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open secondary authentication file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open included authentication file "\.\.\/not_a_file" as "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open secondary authentication file \"\@dbnames_fails.conf\" as \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	'could not open configuration file "recurse.conf": maximum nesting depth exceeded');
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open secondary authentication file \"\@osnames_fails.conf\" as \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	'could not open configuration file "ident_recurse.conf": maximum nesting depth exceeded');
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT mapping_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY mapping_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 79408710e0..5ed2fe3704 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v8-0006-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From 60df23e03e07397054fe71a224f8302524d52741 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v8 6/6] POC: Add a pg_hba_matches() function.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 73da687d5d..bfee72f705 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 814bfcf30d..70e7c871a8 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -27,6 +27,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -42,6 +43,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2983,3 +2985,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d66b2443a4..f6609a4a5f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6139,6 +6139,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.37.0

#44Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#42)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Jul 26, 2022 at 1:14 PM Michael Paquier <michael@paquier.xyz> wrote:

On Tue, Jul 26, 2022 at 01:04:02PM +0800, Julien Rouhaud wrote:

It doesn't have much impact most of the time. The filename is reported if
there's an IO error while reading the already opened correct file. The real
problem is if the hba_file and ident_file are stored in different directory,
any secondary file (@filename) in the pg_ident.conf would be searched in the
wrong directory. With the pending file inclusion patchset, the problem is
immediately visible as the view is reporting the wrong file name.

Oops, obviously. I'll go and fix that as that's on me.

Simple fix attached. I'll add a v15 open item shortly.

Thanks. Better not to lose track of it.

For the archives's sake, this was fixed shortly after as of
27e0ee57f68d27af68967759a2ff61a581f501dc on master and the open item
is closed.

#45Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#43)
Re: Allow file inclusion in pg_hba and pg_ident files

On Sat, Jul 30, 2022 at 04:09:36PM +0800, Julien Rouhaud wrote:

I've been working on all of that and came up with the attached v8.

- 0001: I modified things as discussed previously to report the real auth file
names rather than the hardcoded "pg_ident.conf" and "pg_hba.conf" in the
various log messages
- 0002 and 0003 are minor fixes to error logs more consistent

As a quick update from my side, I intend to look and apply 0001~0003
(not double-checked yet) shortly.
--
Michael

#46Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#45)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Aug 02, 2022 at 07:32:54PM +0900, Michael Paquier wrote:

As a quick update from my side, I intend to look and apply 0001~0003
(not double-checked yet) shortly.

And a couple of days later, these look fine so done as of 47ab1ac and
718fe0a. 0002 and 0003 have been merged together.
--
Michael

#47Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#46)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Fri, Aug 05, 2022 at 09:56:29AM +0900, Michael Paquier wrote:

On Tue, Aug 02, 2022 at 07:32:54PM +0900, Michael Paquier wrote:

As a quick update from my side, I intend to look and apply 0001~0003
(not double-checked yet) shortly.

And a couple of days later, these look fine so done as of 47ab1ac and
718fe0a. 0002 and 0003 have been merged together.

Thanks!

#48Julien Rouhaud
rjuju123@gmail.com
In reply to: Julien Rouhaud (#43)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Sat, Jul 30, 2022 at 04:09:36PM +0800, Julien Rouhaud wrote:

- 0001: the rule_number / mapping_number addition in the views in a separate
commit
- 0002: the main file inclusion patch. Only a few minor bugfix since
previous version discovered thanks to the tests (a bit more about it after),
and documentation tweaks based on previous discussions
- 0003: the pg_hba_matches() POC, no changes

Attached v9, simple rebase after multiple conflicts and part of the patchset
applied.

Attachments:

v9-0001-Add-rule_number-mapping_number-to-the-pg_hba-pg_i.patchtext/plain; charset=us-asciiDownload
From 5430fd3b829b9c7600b61095752d4f7f85d1150d Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 10:59:51 +0800
Subject: [PATCH v9 1/3] Add rule_number / mapping_number to the
 pg_hba/pg_ident views.

Author: Julien Rouhaud
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/system-views.sgml      | 22 +++++++++++++
 src/backend/utils/adt/hbafuncs.c    | 50 ++++++++++++++++++++++-------
 src/include/catalog/pg_proc.dat     | 11 ++++---
 src/test/regress/expected/rules.out | 10 +++---
 4 files changed, 72 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 44aa70a031..1d619427c1 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -991,6 +991,18 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number of this rule among all rules if the rule is valid, otherwise
+       null. This indicates the order in which each rule will be considered
+       until the first matching one, if any, is used to perform authentication
+       with the client.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
@@ -1131,6 +1143,16 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Mapping number, in priority order, of this mapping if the mapping is
+       valid, otherwise null
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 9e5794071c..c9be4bff1f 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,10 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, int lineno, HbaLine *hba,
+						  const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int lineno, IdentLine *ident, const char *err_msg);
+							int mapping_number, int lineno, IdentLine *ident,
+							const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 10
 
 /*
  * fill_hba_line
@@ -165,6 +167,7 @@ get_hba_options(HbaLine *hba)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * rule_number: unique rule identifier among all valid rules
  * lineno: pg_hba.conf line number (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -174,7 +177,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +197,11 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +345,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +368,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,7 +403,11 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
+		/* No error, set a new rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
 					  hbaline, tok_line->err_msg);
 	}
 
@@ -430,8 +444,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -439,6 +453,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * mapping_number: unique rule identifier among all valid rules
  * lineno: pg_ident.conf line number (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, int lineno, IdentLine *ident,
+				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,11 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +494,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +516,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,8 +551,12 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-						tok_line->err_msg);
+		/* No error, set a new mapping number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->line_num, identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be47583122..b284212bd3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6128,15 +6128,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o}',
+  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7ec3d2688f..79408710e0 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,8 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1347,14 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v9-0002-Allow-file-inclusion-in-pg_hba-and-pg_ident-files.patchtext/plain; charset=us-asciiDownload
From f50ef9722e38e2fd1f52cf8e6b31a126ecd811c7 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 11:15:06 +0800
Subject: [PATCH v9 2/3] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  22 +-
 src/backend/libpq/hba.c                       | 483 ++++++++++---
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  43 +-
 src/backend/utils/misc/guc-file.l             | 250 +++----
 src/include/catalog/pg_proc.dat               |  12 +-
 src/include/libpq/hba.h                       |   5 +-
 src/include/utils/guc.h                       |   2 +
 .../authentication/t/003_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 12 files changed, 1324 insertions(+), 282 deletions(-)
 create mode 100644 src/test/authentication/t/003_file_inclusion.pl

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 433759928b..e4eacab4a5 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -837,8 +888,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -849,6 +902,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1d619427c1..4e63dec74c 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1003,12 +1003,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1153,12 +1162,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this mapping
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_ident.conf</filename>
+       Line number of this mapping in the given <literal>file_name</literal>
       </para></entry>
      </row>
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 857b9e5eb2..49a8c56f41 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -22,6 +22,7 @@
 #include <sys/param.h>
 #include <sys/socket.h>
 #include <netdb.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -69,6 +70,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -113,10 +120,22 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int depth, int elevel,
+							   char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   bool strict, const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -303,7 +322,7 @@ copy_auth_token(AuthToken *in)
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int depth, int elevel, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -319,7 +338,7 @@ next_field_expand(const char *filename, char **lineptr,
 
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
-			tokens = tokenize_inc_file(tokens, filename, buf + 1,
+			tokens = tokenize_inc_file(tokens, filename, buf + 1, depth + 1,
 									   elevel, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
@@ -347,6 +366,7 @@ static List *
 tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
+				  int depth,
 				  int elevel,
 				  char **err_msg)
 {
@@ -356,39 +376,30 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
 	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
+		*err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", *err_msg)));
+		return tokens;
 	}
 
-	inc_file = AllocateFile(inc_fullname, "r");
-	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, true,
+							 outer_filename, elevel, err_msg, &inc_fullname);
 
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
+	if (inc_file == NULL)
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, depth + 1,
+								 elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -426,11 +437,38 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -439,30 +477,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int depth, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -515,7 +545,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		{
 			List	   *current_field;
 
-			current_field = next_field_expand(filename, &lineptr,
+			current_field = next_field_expand(filename, &lineptr, depth,
 											  elevel, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
@@ -523,29 +553,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetDirConfFiles(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -860,7 +988,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -880,7 +1008,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -903,7 +1031,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry at end of line"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = psprintf("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -916,7 +1044,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = psprintf("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -939,6 +1067,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -953,6 +1082,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -967,7 +1097,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for connection type"),
 				 errhint("Specify exactly one connection type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for connection type";
 		return NULL;
 	}
@@ -995,7 +1125,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("hostssl record cannot match because SSL is disabled"),
 						 errhint("Set ssl = on in postgresql.conf."),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "hostssl record cannot match because SSL is disabled";
 			}
 #else
@@ -1003,7 +1133,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostssl record cannot match because SSL is not supported by this build";
 #endif
 		}
@@ -1015,7 +1145,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
 #endif
 		}
@@ -1036,7 +1166,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid connection type \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid connection type \"%s\"", token->string);
 		return NULL;
 	}
@@ -1049,7 +1179,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before database specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before database specification";
 		return NULL;
 	}
@@ -1069,7 +1199,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before role specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before role specification";
 		return NULL;
 	}
@@ -1091,7 +1221,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("end-of-line before IP address specification"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "end-of-line before IP address specification";
 			return NULL;
 		}
@@ -1103,7 +1233,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("multiple values specified for host address"),
 					 errhint("Specify one address range per line."),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "multiple values specified for host address";
 			return NULL;
 		}
@@ -1162,7 +1292,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("invalid IP address \"%s\": %s",
 								str, gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("invalid IP address \"%s\": %s",
 									str, gai_strerror(ret));
 				if (gai_result)
@@ -1182,7 +1312,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
 										token->string);
 					return NULL;
@@ -1196,7 +1326,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid CIDR mask in address \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid CIDR mask in address \"%s\"",
 										token->string);
 					return NULL;
@@ -1216,7 +1346,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("end-of-line before netmask specification"),
 							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "end-of-line before netmask specification";
 					return NULL;
 				}
@@ -1227,7 +1357,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("multiple values specified for netmask"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "multiple values specified for netmask";
 					return NULL;
 				}
@@ -1242,7 +1372,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid IP mask \"%s\": %s",
 									token->string, gai_strerror(ret)),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid IP mask \"%s\": %s",
 										token->string, gai_strerror(ret));
 					if (gai_result)
@@ -1261,7 +1391,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("IP address and mask do not match"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "IP address and mask do not match";
 					return NULL;
 				}
@@ -1277,7 +1407,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before authentication method"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before authentication method";
 		return NULL;
 	}
@@ -1289,7 +1419,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for authentication type"),
 				 errhint("Specify exactly one authentication type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for authentication type";
 		return NULL;
 	}
@@ -1326,7 +1456,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
 			return NULL;
 		}
@@ -1367,7 +1497,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\"",
 							token->string);
 		return NULL;
@@ -1380,7 +1510,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\": not supported by this build",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
 							token->string);
 		return NULL;
@@ -1402,7 +1532,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("gssapi authentication is not supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "gssapi authentication is not supported on local sockets";
 		return NULL;
 	}
@@ -1414,7 +1544,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("peer authentication is only supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "peer authentication is only supported on local sockets";
 		return NULL;
 	}
@@ -1432,7 +1562,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("cert authentication is only supported on hostssl connections"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "cert authentication is only supported on hostssl connections";
 		return NULL;
 	}
@@ -1482,7 +1612,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("authentication option not in name=value format: %s", token->string),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("authentication option not in name=value format: %s",
 									token->string);
 				return NULL;
@@ -1526,7 +1656,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
 				return NULL;
 			}
@@ -1537,7 +1667,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(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)));
+								line_num, file_name)));
 			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
 			return NULL;
 		}
@@ -1553,7 +1683,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
 			return NULL;
 		}
@@ -1570,7 +1700,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS servers cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS servers cannot be empty";
 			return NULL;
 		}
@@ -1581,7 +1711,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS secrets cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS secrets cannot be empty";
 			return NULL;
 		}
@@ -1600,7 +1730,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiussecrets),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiussecrets),
 								list_length(parsedline->radiusservers));
@@ -1616,7 +1746,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusports),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusports),
 								list_length(parsedline->radiusservers));
@@ -1632,7 +1762,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusidentifiers),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusidentifiers),
 								list_length(parsedline->radiusservers));
@@ -1667,6 +1797,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -1690,7 +1821,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientcert can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1707,7 +1838,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
 				return false;
 			}
@@ -1720,7 +1851,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientcert: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1732,7 +1863,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientname can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientname can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1751,7 +1882,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientname: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1837,7 +1968,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid ldapscheme value: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 		hbaline->ldapscheme = pstrdup(val);
 	}
 	else if (strcmp(name, "ldapserver") == 0)
@@ -1855,7 +1986,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid LDAP port number: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid LDAP port number: \"%s\"", val);
 			return false;
 		}
@@ -1949,7 +2080,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS server list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -1968,7 +2099,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
 								(char *) lfirst(l), gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				if (gai_result)
 					pg_freeaddrinfo_all(hints.ai_family, gai_result);
 
@@ -1997,7 +2128,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS port list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
 			return false;
 		}
@@ -2010,7 +2141,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("invalid RADIUS port number: \"%s\"", val),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 
 				return false;
 			}
@@ -2033,7 +2164,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS secret list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2055,7 +2186,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS identifiers list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2069,7 +2200,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				 errmsg("unrecognized authentication option name: \"%s\"",
 						name),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
 							name);
 		return false;
@@ -2217,7 +2348,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2288,6 +2419,137 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename, bool strict,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		if (strict)
+		{
+			switch (kind)
+			{
+				case SecondaryAuthFile:
+					msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+					msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+					break;
+				case IncludedAuthFile:
+					msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+					msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+					break;
+				default:
+					elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+					break;
+			}
+
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg(msglog, inc_filename, *inc_fullname)));
+			*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+								strerror(save_errno));
+		}
+		else
+		{
+			Assert(kind == IncludedAuthFile);
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							*inc_fullname)));
+		}
+
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		char *err_msg;
+
+		err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", err_msg)));
+		return err_msg;
+	}
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, strict,
+							 outer_filename, elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_inc_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+			return NULL;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, depth, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2306,6 +2568,7 @@ load_hba(void)
 IdentLine *
 parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
+	char	   *file_name = tok_line->file_name;
 	int			line_num = tok_line->line_num;
 	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
@@ -2366,7 +2629,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("invalid regular expression \"%s\": %s",
 							parsedline->ident_user + 1, errstr),
 					 errcontext("line %d of configuration file \"%s\"",
-							line_num, IdentFileName)));
+							line_num, file_name)));
 
 			*err_msg = psprintf("invalid regular expression \"%s\": %s",
 								parsedline->ident_user + 1, errstr);
@@ -2601,7 +2864,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index c9be4bff1f..15326a01e2 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int mapping_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int mapping_number, const char *filename,
+							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique rule identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -202,6 +203,8 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -345,7 +348,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -386,7 +389,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open configuration file \"%s\": %m",
 						HbaFileName)));
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -407,8 +410,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			rule_number++;
 
-		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -445,7 +448,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -454,7 +457,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * mapping_number: unique rule identifier among all valid rules
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -463,8 +467,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int mapping_number, int lineno, IdentLine *ident,
-				const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -482,6 +486,8 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -494,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -534,7 +540,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open usermap file \"%s\": %m",
 						IdentFileName)));
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -556,7 +562,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			mapping_number++;
 
 		fill_ident_line(tuple_store, tupdesc, mapping_number,
-						tok_line->line_num, identline, tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index ce5633844c..b3e18e48cf 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -700,6 +700,122 @@ GUC_flex_fatal(const char *msg)
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * Returns the list of config files located in a directory, in alphabetical
+ * order.
+ *
+ * We don't check for recursion or too-deep nesting depth here, its up to the
+ * caller to take care of that.
+ */
+char **
+GetDirConfFiles(const char *includedir, const char *calling_file, int elevel,
+				int *num_filenames, char **err_msg)
+{
+	char	   *directory;
+	DIR		   *d;
+	struct dirent *de;
+	char	  **filenames;
+	int			size_filenames;
+
+	/*
+	 * Reject directory name that is all-blank (including empty), as that
+	 * leads to confusion --- we'd read the containing directory, typically
+	 * resulting in recursive inclusion of the same file(s).
+	 */
+	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty configuration directory name: \"%s\"",
+						includedir)));
+		*err_msg = "empty configuration directory name";
+		return NULL;
+	}
+
+	directory = AbsoluteConfigLocation(includedir, calling_file);
+	d = AllocateDir(directory);
+	if (d == NULL)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration directory \"%s\": %m",
+						directory)));
+		*err_msg = psprintf("could not open directory \"%s\"", directory);
+		filenames = NULL;
+		goto cleanup;
+	}
+
+	/*
+	 * Read the directory and put the filenames in an array, so we can sort
+	 * them prior to caller processing the contents.
+	 */
+	size_filenames = 32;
+	filenames = (char **) palloc(size_filenames * sizeof(char *));
+	*num_filenames = 0;
+
+	while ((de = ReadDir(d, directory)) != NULL)
+	{
+		struct stat st;
+		char		filename[MAXPGPATH];
+
+		/*
+		 * Only parse files with names ending in ".conf".  Explicitly reject
+		 * files starting with ".".  This excludes things like "." and "..",
+		 * as well as typical hidden files, backup files, and editor debris.
+		 */
+		if (strlen(de->d_name) < 6)
+			continue;
+		if (de->d_name[0] == '.')
+			continue;
+		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+			continue;
+
+		join_path_components(filename, directory, de->d_name);
+		canonicalize_path(filename);
+		if (stat(filename, &st) == 0)
+		{
+			/* Ignore directories. */
+			if (S_ISDIR(st.st_mode))
+				continue;
+
+			/* Add file to array, increasing its size in blocks of 32 */
+			if (*num_filenames >= size_filenames)
+			{
+				size_filenames += 32;
+				filenames = (char **) repalloc(filenames,
+										size_filenames * sizeof(char *));
+			}
+			filenames[*num_filenames] = pstrdup(filename);
+			(*num_filenames)++;
+		}
+		else
+		{
+			/*
+			 * stat does not care about permissions, so the most likely reason
+			 * a file can't be accessed now is if it was removed between the
+			 * directory listing and now.
+			 */
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg("could not stat file \"%s\": %m",
+							filename)));
+			*err_msg = psprintf("could not stat file \"%s\"", filename);
+			pfree(filenames);
+			filenames = NULL;
+			goto cleanup;
+		}
+	}
+
+	if (*num_filenames > 0)
+		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
+
+cleanup:
+	if (d)
+		FreeDir(d);
+	pfree(directory);
+	return filenames;
+}
+
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -961,138 +1077,32 @@ ParseConfigDirectory(const char *includedir,
 					 ConfigVariable **head_p,
 					 ConfigVariable **tail_p)
 {
-	char	   *directory;
-	DIR		   *d;
-	struct dirent *de;
+	char	   *err_msg;
 	char	  **filenames;
 	int			num_filenames;
-	int			size_filenames;
-	bool		status;
 
-	/*
-	 * Reject directory name that is all-blank (including empty), as that
-	 * leads to confusion --- we'd read the containing directory, typically
-	 * resulting in recursive inclusion of the same file(s).
-	 */
-	if (strspn(includedir, " \t\r\n") == strlen(includedir))
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("empty configuration directory name: \"%s\"",
-						includedir)));
-		record_config_file_error("empty configuration directory name",
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		return false;
-	}
+	filenames = GetDirConfFiles(includedir, calling_file, elevel,
+							   &num_filenames, &err_msg);
 
-	/*
-	 * We don't check for recursion or too-deep nesting depth here; the
-	 * subsequent calls to ParseConfigFile will take care of that.
-	 */
-
-	directory = AbsoluteConfigLocation(includedir, calling_file);
-	d = AllocateDir(directory);
-	if (d == NULL)
+	if (!filenames)
 	{
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration directory \"%s\": %m",
-						directory)));
-		record_config_file_error(psprintf("could not open directory \"%s\"",
-										  directory),
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		status = false;
-		goto cleanup;
-	}
-
-	/*
-	 * Read the directory and put the filenames in an array, so we can sort
-	 * them prior to processing the contents.
-	 */
-	size_filenames = 32;
-	filenames = (char **) palloc(size_filenames * sizeof(char *));
-	num_filenames = 0;
-
-	while ((de = ReadDir(d, directory)) != NULL)
-	{
-		struct stat st;
-		char		filename[MAXPGPATH];
-
-		/*
-		 * Only parse files with names ending in ".conf".  Explicitly reject
-		 * files starting with ".".  This excludes things like "." and "..",
-		 * as well as typical hidden files, backup files, and editor debris.
-		 */
-		if (strlen(de->d_name) < 6)
-			continue;
-		if (de->d_name[0] == '.')
-			continue;
-		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
-			continue;
-
-		join_path_components(filename, directory, de->d_name);
-		canonicalize_path(filename);
-		if (stat(filename, &st) == 0)
-		{
-			if (!S_ISDIR(st.st_mode))
-			{
-				/* Add file to array, increasing its size in blocks of 32 */
-				if (num_filenames >= size_filenames)
-				{
-					size_filenames += 32;
-					filenames = (char **) repalloc(filenames,
-											size_filenames * sizeof(char *));
-				}
-				filenames[num_filenames] = pstrdup(filename);
-				num_filenames++;
-			}
-		}
-		else
-		{
-			/*
-			 * stat does not care about permissions, so the most likely reason
-			 * a file can't be accessed now is if it was removed between the
-			 * directory listing and now.
-			 */
-			ereport(elevel,
-					(errcode_for_file_access(),
-					 errmsg("could not stat file \"%s\": %m",
-							filename)));
-			record_config_file_error(psprintf("could not stat file \"%s\"",
-											  filename),
-									 calling_file, calling_lineno,
-									 head_p, tail_p);
-			status = false;
-			goto cleanup;
-		}
+		record_config_file_error(err_msg, calling_file, calling_lineno, head_p,
+								 tail_p);
+		return false;
 	}
 
-	if (num_filenames > 0)
+	for (int i = 0; i < num_filenames; i++)
 	{
-		int			i;
-
-		qsort(filenames, num_filenames, sizeof(char *), pg_qsort_strcmp);
-		for (i = 0; i < num_filenames; i++)
+		if (!ParseConfigFile(filenames[i], true,
+							 calling_file, calling_lineno,
+							 depth, elevel,
+							 head_p, tail_p))
 		{
-			if (!ParseConfigFile(filenames[i], true,
-								 calling_file, calling_lineno,
-								 depth, elevel,
-								 head_p, tail_p))
-			{
-				status = false;
-				goto cleanup;
-			}
+			return false;
 		}
 	}
-	status = true;
 
-cleanup:
-	if (d)
-		FreeDir(d);
-	pfree(directory);
-	return status;
+	return true;
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index b284212bd3..4470011977 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6128,16 +6128,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 90036f7bcd..0ea100d1b8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -155,6 +156,7 @@ typedef struct AuthToken
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
@@ -174,6 +176,7 @@ extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int depth,
+										int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index e734493a48..1a3ab6306d 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -145,6 +145,8 @@ typedef struct ConfigVariable
 	struct ConfigVariable *next;
 } ConfigVariable;
 
+extern char **GetDirConfFiles(const char *includedir, const char *calling_file,
+							  int elevel, int *num_filenames, char **err_msg);
 extern bool ParseConfigFile(const char *config_file, bool strict,
 							const char *calling_file, int calling_lineno,
 							int depth, int elevel,
diff --git a/src/test/authentication/t/003_file_inclusion.pl b/src/test/authentication/t/003_file_inclusion.pl
new file mode 100644
index 0000000000..8eae72b8d4
--- /dev/null
+++ b/src/test/authentication/t/003_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open secondary authentication file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open included authentication file "\.\.\/not_a_file" as "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open secondary authentication file \"\@dbnames_fails.conf\" as \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	'could not open configuration file "recurse.conf": maximum nesting depth exceeded');
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open secondary authentication file \"\@osnames_fails.conf\" as \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	'could not open configuration file "ident_recurse.conf": maximum nesting depth exceeded');
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT mapping_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY mapping_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 79408710e0..5ed2fe3704 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v9-0003-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From 7d662b2c646506c045de340341e1d75139d12b43 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v9 3/3] POC: Add a pg_hba_matches() function.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 30a048f6b0..224b7a483a 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 49a8c56f41..a34b315134 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -28,6 +28,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -43,6 +44,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2974,3 +2976,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4470011977..ba632698fe 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6139,6 +6139,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.37.0

#49Julien Rouhaud
rjuju123@gmail.com
In reply to: Julien Rouhaud (#48)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Aug 16, 2022 at 02:10:30PM +0800, Julien Rouhaud wrote:

Hi,

On Sat, Jul 30, 2022 at 04:09:36PM +0800, Julien Rouhaud wrote:

- 0001: the rule_number / mapping_number addition in the views in a separate
commit
- 0002: the main file inclusion patch. Only a few minor bugfix since
previous version discovered thanks to the tests (a bit more about it after),
and documentation tweaks based on previous discussions
- 0003: the pg_hba_matches() POC, no changes

v10 attached to fix a few conflict with recent refactoring commits, no other
change.

Attachments:

v10-0001-Add-rule_number-mapping_number-to-the-pg_hba-pg_.patchtext/plain; charset=us-asciiDownload
From d81715bf9848f1aa310ea199a9774eebcbe372ab Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 10:59:51 +0800
Subject: [PATCH v10 1/3] Add rule_number / mapping_number to the
 pg_hba/pg_ident views.

Author: Julien Rouhaud
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/system-views.sgml      | 22 +++++++++++++
 src/backend/utils/adt/hbafuncs.c    | 50 ++++++++++++++++++++++-------
 src/include/catalog/pg_proc.dat     | 11 ++++---
 src/test/regress/expected/rules.out | 10 +++---
 4 files changed, 72 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 44aa70a031..1d619427c1 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -991,6 +991,18 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number of this rule among all rules if the rule is valid, otherwise
+       null. This indicates the order in which each rule will be considered
+       until the first matching one, if any, is used to perform authentication
+       with the client.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
@@ -1131,6 +1143,16 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Mapping number, in priority order, of this mapping if the mapping is
+       valid, otherwise null
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 9e5794071c..c9be4bff1f 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,10 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, int lineno, HbaLine *hba,
+						  const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int lineno, IdentLine *ident, const char *err_msg);
+							int mapping_number, int lineno, IdentLine *ident,
+							const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 10
 
 /*
  * fill_hba_line
@@ -165,6 +167,7 @@ get_hba_options(HbaLine *hba)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * rule_number: unique rule identifier among all valid rules
  * lineno: pg_hba.conf line number (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -174,7 +177,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +197,11 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +345,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +368,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,7 +403,11 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
+		/* No error, set a new rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
 					  hbaline, tok_line->err_msg);
 	}
 
@@ -430,8 +444,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -439,6 +453,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * mapping_number: unique rule identifier among all valid rules
  * lineno: pg_ident.conf line number (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, int lineno, IdentLine *ident,
+				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,11 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +494,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +516,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,8 +551,12 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-						tok_line->err_msg);
+		/* No error, set a new mapping number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->line_num, identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index a07e737a33..060bafaced 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6128,15 +6128,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o}',
+  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7ec3d2688f..79408710e0 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,8 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1347,14 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v10-0002-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/plain; charset=us-asciiDownload
From e7cdde8d69bb2dfb9d338615baa687cc5321dc22 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 11:15:06 +0800
Subject: [PATCH v10 2/3] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  22 +-
 src/backend/libpq/hba.c                       | 483 ++++++++++---
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  43 +-
 src/backend/utils/misc/guc-file.l             | 229 +++---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/include/libpq/hba.h                       |   5 +-
 src/include/utils/guc.h                       |   2 +
 .../authentication/t/003_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 12 files changed, 1312 insertions(+), 273 deletions(-)
 create mode 100644 src/test/authentication/t/003_file_inclusion.pl

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index c6f1b70fd3..42ceb6f3e6 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -835,8 +886,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -847,6 +900,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1d619427c1..4e63dec74c 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1003,12 +1003,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1153,12 +1162,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this mapping
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_ident.conf</filename>
+       Line number of this mapping in the given <literal>file_name</literal>
       </para></entry>
      </row>
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 4637426d62..071bf1ff95 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -22,6 +22,7 @@
 #include <sys/param.h>
 #include <sys/socket.h>
 #include <netdb.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -69,6 +70,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -113,10 +120,22 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int depth, int elevel,
+							   char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   bool strict, const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -303,7 +322,7 @@ copy_auth_token(AuthToken *in)
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int depth, int elevel, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -319,7 +338,7 @@ next_field_expand(const char *filename, char **lineptr,
 
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
-			tokens = tokenize_inc_file(tokens, filename, buf + 1,
+			tokens = tokenize_inc_file(tokens, filename, buf + 1, depth + 1,
 									   elevel, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
@@ -347,6 +366,7 @@ static List *
 tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
+				  int depth,
 				  int elevel,
 				  char **err_msg)
 {
@@ -356,39 +376,30 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
 	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
+		*err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", *err_msg)));
+		return tokens;
 	}
 
-	inc_file = AllocateFile(inc_fullname, "r");
-	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, true,
+							 outer_filename, elevel, err_msg, &inc_fullname);
 
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
+	if (inc_file == NULL)
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, depth + 1,
+								 elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -426,11 +437,38 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -439,30 +477,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int depth, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -515,7 +545,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		{
 			List	   *current_field;
 
-			current_field = next_field_expand(filename, &lineptr,
+			current_field = next_field_expand(filename, &lineptr, depth,
 											  elevel, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
@@ -523,29 +553,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetDirConfFiles(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -855,7 +983,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -875,7 +1003,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -898,7 +1026,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry at end of line"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -911,7 +1039,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -934,6 +1062,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -948,6 +1077,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -962,7 +1092,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for connection type"),
 				 errhint("Specify exactly one connection type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for connection type";
 		return NULL;
 	}
@@ -990,7 +1120,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("hostssl record cannot match because SSL is disabled"),
 						 errhint("Set ssl = on in postgresql.conf."),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "hostssl record cannot match because SSL is disabled";
 			}
 #else
@@ -998,7 +1128,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostssl record cannot match because SSL is not supported by this build";
 #endif
 		}
@@ -1010,7 +1140,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
 #endif
 		}
@@ -1031,7 +1161,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid connection type \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid connection type \"%s\"", token->string);
 		return NULL;
 	}
@@ -1044,7 +1174,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before database specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before database specification";
 		return NULL;
 	}
@@ -1064,7 +1194,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before role specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before role specification";
 		return NULL;
 	}
@@ -1086,7 +1216,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("end-of-line before IP address specification"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "end-of-line before IP address specification";
 			return NULL;
 		}
@@ -1098,7 +1228,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("multiple values specified for host address"),
 					 errhint("Specify one address range per line."),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "multiple values specified for host address";
 			return NULL;
 		}
@@ -1157,7 +1287,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("invalid IP address \"%s\": %s",
 								str, gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("invalid IP address \"%s\": %s",
 									str, gai_strerror(ret));
 				if (gai_result)
@@ -1177,7 +1307,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
 										token->string);
 					return NULL;
@@ -1191,7 +1321,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid CIDR mask in address \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid CIDR mask in address \"%s\"",
 										token->string);
 					return NULL;
@@ -1211,7 +1341,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("end-of-line before netmask specification"),
 							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "end-of-line before netmask specification";
 					return NULL;
 				}
@@ -1222,7 +1352,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("multiple values specified for netmask"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "multiple values specified for netmask";
 					return NULL;
 				}
@@ -1237,7 +1367,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid IP mask \"%s\": %s",
 									token->string, gai_strerror(ret)),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid IP mask \"%s\": %s",
 										token->string, gai_strerror(ret));
 					if (gai_result)
@@ -1256,7 +1386,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("IP address and mask do not match"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "IP address and mask do not match";
 					return NULL;
 				}
@@ -1272,7 +1402,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before authentication method"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before authentication method";
 		return NULL;
 	}
@@ -1284,7 +1414,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for authentication type"),
 				 errhint("Specify exactly one authentication type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for authentication type";
 		return NULL;
 	}
@@ -1321,7 +1451,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
 			return NULL;
 		}
@@ -1362,7 +1492,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\"",
 							token->string);
 		return NULL;
@@ -1375,7 +1505,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\": not supported by this build",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
 							token->string);
 		return NULL;
@@ -1397,7 +1527,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("gssapi authentication is not supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "gssapi authentication is not supported on local sockets";
 		return NULL;
 	}
@@ -1409,7 +1539,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("peer authentication is only supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "peer authentication is only supported on local sockets";
 		return NULL;
 	}
@@ -1427,7 +1557,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("cert authentication is only supported on hostssl connections"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "cert authentication is only supported on hostssl connections";
 		return NULL;
 	}
@@ -1477,7 +1607,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("authentication option not in name=value format: %s", token->string),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("authentication option not in name=value format: %s",
 									token->string);
 				return NULL;
@@ -1521,7 +1651,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
 				return NULL;
 			}
@@ -1532,7 +1662,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(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)));
+								line_num, file_name)));
 			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
 			return NULL;
 		}
@@ -1548,7 +1678,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
 			return NULL;
 		}
@@ -1565,7 +1695,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS servers cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS servers cannot be empty";
 			return NULL;
 		}
@@ -1576,7 +1706,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS secrets cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS secrets cannot be empty";
 			return NULL;
 		}
@@ -1595,7 +1725,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiussecrets),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiussecrets),
 								list_length(parsedline->radiusservers));
@@ -1611,7 +1741,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusports),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusports),
 								list_length(parsedline->radiusservers));
@@ -1627,7 +1757,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusidentifiers),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusidentifiers),
 								list_length(parsedline->radiusservers));
@@ -1662,6 +1792,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -1685,7 +1816,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientcert can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1702,7 +1833,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
 				return false;
 			}
@@ -1715,7 +1846,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientcert: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1727,7 +1858,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientname can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientname can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1746,7 +1877,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientname: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1832,7 +1963,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid ldapscheme value: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 		hbaline->ldapscheme = pstrdup(val);
 	}
 	else if (strcmp(name, "ldapserver") == 0)
@@ -1850,7 +1981,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid LDAP port number: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid LDAP port number: \"%s\"", val);
 			return false;
 		}
@@ -1944,7 +2075,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS server list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -1963,7 +2094,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
 								(char *) lfirst(l), gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				if (gai_result)
 					pg_freeaddrinfo_all(hints.ai_family, gai_result);
 
@@ -1992,7 +2123,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS port list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
 			return false;
 		}
@@ -2005,7 +2136,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("invalid RADIUS port number: \"%s\"", val),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 
 				return false;
 			}
@@ -2028,7 +2159,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS secret list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2050,7 +2181,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS identifiers list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2064,7 +2195,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				 errmsg("unrecognized authentication option name: \"%s\"",
 						name),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
 							name);
 		return false;
@@ -2212,7 +2343,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2283,6 +2414,137 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename, bool strict,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		if (strict)
+		{
+			switch (kind)
+			{
+				case SecondaryAuthFile:
+					msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+					msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+					break;
+				case IncludedAuthFile:
+					msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+					msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+					break;
+				default:
+					elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+					break;
+			}
+
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg(msglog, inc_filename, *inc_fullname)));
+			*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+								strerror(save_errno));
+		}
+		else
+		{
+			Assert(kind == IncludedAuthFile);
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							*inc_fullname)));
+		}
+
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		char *err_msg;
+
+		err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", err_msg)));
+		return err_msg;
+	}
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, strict,
+							 outer_filename, elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_inc_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+			return NULL;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, depth, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2301,6 +2563,7 @@ load_hba(void)
 IdentLine *
 parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
+	char	   *file_name = tok_line->file_name;
 	int			line_num = tok_line->line_num;
 	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
@@ -2361,7 +2624,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("invalid regular expression \"%s\": %s",
 							parsedline->ident_user + 1, errstr),
 					 errcontext("line %d of configuration file \"%s\"",
-							line_num, IdentFileName)));
+							line_num, file_name)));
 
 			*err_msg = psprintf("invalid regular expression \"%s\": %s",
 								parsedline->ident_user + 1, errstr);
@@ -2596,7 +2859,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index c9be4bff1f..15326a01e2 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int mapping_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int mapping_number, const char *filename,
+							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique rule identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -202,6 +203,8 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -345,7 +348,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -386,7 +389,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open configuration file \"%s\": %m",
 						HbaFileName)));
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -407,8 +410,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			rule_number++;
 
-		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -445,7 +448,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -454,7 +457,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * mapping_number: unique rule identifier among all valid rules
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -463,8 +467,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int mapping_number, int lineno, IdentLine *ident,
-				const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -482,6 +486,8 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -494,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -534,7 +540,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open usermap file \"%s\": %m",
 						IdentFileName)));
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -556,7 +562,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			mapping_number++;
 
 		fill_ident_line(tuple_store, tupdesc, mapping_number,
-						tok_line->line_num, identline, tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index 4f43430db4..ecfb4a2a28 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -345,6 +345,110 @@ GUC_flex_fatal(const char *msg)
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * Returns the list of config files located in a directory, in alphabetical
+ * order.
+ *
+ * We don't check for recursion or too-deep nesting depth here, its up to the
+ * caller to take care of that.
+ */
+char **
+GetDirConfFiles(const char *includedir, const char *calling_file, int elevel,
+				int *num_filenames, char **err_msg)
+{
+	char	   *directory;
+	DIR		   *d;
+	struct dirent *de;
+	char	  **filenames;
+	int			size_filenames;
+
+	/*
+	 * Reject directory name that is all-blank (including empty), as that
+	 * leads to confusion --- we'd read the containing directory, typically
+	 * resulting in recursive inclusion of the same file(s).
+	 */
+	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty configuration directory name: \"%s\"",
+						includedir)));
+		*err_msg = "empty configuration directory name";
+		return NULL;
+	}
+
+	directory = AbsoluteConfigLocation(includedir, calling_file);
+	d = AllocateDir(directory);
+	if (d == NULL)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration directory \"%s\": %m",
+						directory)));
+		*err_msg = psprintf("could not open directory \"%s\"", directory);
+		filenames = NULL;
+		goto cleanup;
+	}
+
+	/*
+	 * Read the directory and put the filenames in an array, so we can sort
+	 * them prior to caller processing the contents.
+	 */
+	size_filenames = 32;
+	filenames = (char **) palloc(size_filenames * sizeof(char *));
+	*num_filenames = 0;
+
+	while ((de = ReadDir(d, directory)) != NULL)
+	{
+		PGFileType	de_type;
+		char		filename[MAXPGPATH];
+
+		/*
+		 * Only parse files with names ending in ".conf".  Explicitly reject
+		 * files starting with ".".  This excludes things like "." and "..",
+		 * as well as typical hidden files, backup files, and editor debris.
+		 */
+		if (strlen(de->d_name) < 6)
+			continue;
+		if (de->d_name[0] == '.')
+			continue;
+		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+			continue;
+
+		join_path_components(filename, directory, de->d_name);
+		canonicalize_path(filename);
+		de_type = get_dirent_type(filename, de, true, elevel);
+		if (de_type == PGFILETYPE_ERROR)
+		{
+			*err_msg = psprintf("could not stat file \"%s\"", filename);
+			pfree(filenames);
+			filenames = NULL;
+			goto cleanup;
+		}
+		else if (de_type != PGFILETYPE_DIR)
+		{
+			/* Add file to array, increasing its size in blocks of 32 */
+			if (*num_filenames >= size_filenames)
+			{
+				size_filenames += 32;
+				filenames = (char **) repalloc(filenames,
+										size_filenames * sizeof(char *));
+			}
+			filenames[*num_filenames] = pstrdup(filename);
+			(*num_filenames)++;
+		}
+	}
+
+	if (*num_filenames > 0)
+		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
+
+cleanup:
+	if (d)
+		FreeDir(d);
+	pfree(directory);
+	return filenames;
+}
+
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -606,127 +710,30 @@ ParseConfigDirectory(const char *includedir,
 					 ConfigVariable **head_p,
 					 ConfigVariable **tail_p)
 {
-	char	   *directory;
-	DIR		   *d;
-	struct dirent *de;
+	char	   *err_msg;
 	char	  **filenames;
 	int			num_filenames;
-	int			size_filenames;
-	bool		status;
-
-	/*
-	 * Reject directory name that is all-blank (including empty), as that
-	 * leads to confusion --- we'd read the containing directory, typically
-	 * resulting in recursive inclusion of the same file(s).
-	 */
-	if (strspn(includedir, " \t\r\n") == strlen(includedir))
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("empty configuration directory name: \"%s\"",
-						includedir)));
-		record_config_file_error("empty configuration directory name",
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		return false;
-	}
-
-	/*
-	 * We don't check for recursion or too-deep nesting depth here; the
-	 * subsequent calls to ParseConfigFile will take care of that.
-	 */
-
-	directory = AbsoluteConfigLocation(includedir, calling_file);
-	d = AllocateDir(directory);
-	if (d == NULL)
-	{
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration directory \"%s\": %m",
-						directory)));
-		record_config_file_error(psprintf("could not open directory \"%s\"",
-										  directory),
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		status = false;
-		goto cleanup;
-	}
 
-	/*
-	 * Read the directory and put the filenames in an array, so we can sort
-	 * them prior to processing the contents.
-	 */
-	size_filenames = 32;
-	filenames = (char **) palloc(size_filenames * sizeof(char *));
-	num_filenames = 0;
+	filenames = GetDirConfFiles(includedir, calling_file, elevel,
+							   &num_filenames, &err_msg);
 
-	while ((de = ReadDir(d, directory)) != NULL)
+	if (!filenames)
 	{
-		PGFileType	de_type;
-		char		filename[MAXPGPATH];
-
-		/*
-		 * Only parse files with names ending in ".conf".  Explicitly reject
-		 * files starting with ".".  This excludes things like "." and "..",
-		 * as well as typical hidden files, backup files, and editor debris.
-		 */
-		if (strlen(de->d_name) < 6)
-			continue;
-		if (de->d_name[0] == '.')
-			continue;
-		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
-			continue;
-
-		join_path_components(filename, directory, de->d_name);
-		canonicalize_path(filename);
-		de_type = get_dirent_type(filename, de, true, elevel);
-		if (de_type == PGFILETYPE_ERROR)
-		{
-			record_config_file_error(psprintf("could not stat file \"%s\"",
-											  filename),
-									 calling_file, calling_lineno,
-									 head_p, tail_p);
-			status = false;
-			goto cleanup;
-		}
-		else if (de_type != PGFILETYPE_DIR)
-		{
-			/* Add file to array, increasing its size in blocks of 32 */
-			if (num_filenames >= size_filenames)
-			{
-				size_filenames += 32;
-				filenames = (char **) repalloc(filenames,
-											   size_filenames * sizeof(char *));
-			}
-			filenames[num_filenames] = pstrdup(filename);
-			num_filenames++;
-		}
+		record_config_file_error(err_msg, calling_file, calling_lineno, head_p,
+								 tail_p);
+		return false;
 	}
 
-	if (num_filenames > 0)
+	for (int i = 0; i < num_filenames; i++)
 	{
-		int			i;
-
-		qsort(filenames, num_filenames, sizeof(char *), pg_qsort_strcmp);
-		for (i = 0; i < num_filenames; i++)
-		{
-			if (!ParseConfigFile(filenames[i], true,
-								 calling_file, calling_lineno,
-								 depth, elevel,
-								 head_p, tail_p))
-			{
-				status = false;
-				goto cleanup;
-			}
-		}
+		if (!ParseConfigFile(filenames[i], true,
+							 calling_file, calling_lineno,
+							 depth, elevel,
+							 head_p, tail_p))
+			return false;
 	}
-	status = true;
 
-cleanup:
-	if (d)
-		FreeDir(d);
-	pfree(directory);
-	return status;
+	return true;
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 060bafaced..e6a54cc3d6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6128,16 +6128,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 90036f7bcd..0ea100d1b8 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -155,6 +156,7 @@ typedef struct AuthToken
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
@@ -174,6 +176,7 @@ extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int depth,
+										int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 45ae1b537f..cb3bbcf949 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -145,6 +145,8 @@ typedef struct ConfigVariable
 	struct ConfigVariable *next;
 } ConfigVariable;
 
+extern char **GetDirConfFiles(const char *includedir, const char *calling_file,
+							  int elevel, int *num_filenames, char **err_msg);
 extern bool ParseConfigFile(const char *config_file, bool strict,
 							const char *calling_file, int calling_lineno,
 							int depth, int elevel,
diff --git a/src/test/authentication/t/003_file_inclusion.pl b/src/test/authentication/t/003_file_inclusion.pl
new file mode 100644
index 0000000000..8eae72b8d4
--- /dev/null
+++ b/src/test/authentication/t/003_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open secondary authentication file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open included authentication file "\.\.\/not_a_file" as "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open secondary authentication file \"\@dbnames_fails.conf\" as \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	'could not open configuration file "recurse.conf": maximum nesting depth exceeded');
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open secondary authentication file \"\@osnames_fails.conf\" as \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	'could not open configuration file "ident_recurse.conf": maximum nesting depth exceeded');
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT mapping_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY mapping_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 79408710e0..5ed2fe3704 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v10-0003-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From 7ec7e8deacd6c60290e70d07b3b171c2d3b82e52 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v10 3/3] POC: Add a pg_hba_matches() function.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 30a048f6b0..224b7a483a 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 071bf1ff95..02a7164225 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -28,6 +28,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -43,6 +44,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2969,3 +2971,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e6a54cc3d6..d78c5a7556 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6139,6 +6139,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.37.0

#50Julien Rouhaud
rjuju123@gmail.com
In reply to: Julien Rouhaud (#49)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Sun, Sep 18, 2022 at 01:06:12AM +0800, Julien Rouhaud wrote:

On Tue, Aug 16, 2022 at 02:10:30PM +0800, Julien Rouhaud wrote:

On Sat, Jul 30, 2022 at 04:09:36PM +0800, Julien Rouhaud wrote:

- 0001: the rule_number / mapping_number addition in the views in a separate
commit
- 0002: the main file inclusion patch. Only a few minor bugfix since
previous version discovered thanks to the tests (a bit more about it after),
and documentation tweaks based on previous discussions
- 0003: the pg_hba_matches() POC, no changes

v11 attached, fixing a shadowed variable warning.

Attachments:

v11-0001-Add-rule_number-mapping_number-to-the-pg_hba-pg_.patchtext/plain; charset=us-asciiDownload
From fc8cfc1327e153ec23860aea63cdd81ff053cba2 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 10:59:51 +0800
Subject: [PATCH v11 1/3] Add rule_number / mapping_number to the
 pg_hba/pg_ident views.

Author: Julien Rouhaud
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/system-views.sgml      | 22 +++++++++++++
 src/backend/utils/adt/hbafuncs.c    | 50 ++++++++++++++++++++++-------
 src/include/catalog/pg_proc.dat     | 11 ++++---
 src/test/regress/expected/rules.out | 10 +++---
 4 files changed, 72 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1ca7c3f9bf..4723f712a7 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -991,6 +991,18 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number of this rule among all rules if the rule is valid, otherwise
+       null. This indicates the order in which each rule will be considered
+       until the first matching one, if any, is used to perform authentication
+       with the client.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
@@ -1131,6 +1143,16 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Mapping number, in priority order, of this mapping if the mapping is
+       valid, otherwise null
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 9e5794071c..c9be4bff1f 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,10 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, int lineno, HbaLine *hba,
+						  const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int lineno, IdentLine *ident, const char *err_msg);
+							int mapping_number, int lineno, IdentLine *ident,
+							const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 10
 
 /*
  * fill_hba_line
@@ -165,6 +167,7 @@ get_hba_options(HbaLine *hba)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * rule_number: unique rule identifier among all valid rules
  * lineno: pg_hba.conf line number (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -174,7 +177,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +197,11 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +345,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +368,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,7 +403,11 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
+		/* No error, set a new rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
 					  hbaline, tok_line->err_msg);
 	}
 
@@ -430,8 +444,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -439,6 +453,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * mapping_number: unique rule identifier among all valid rules
  * lineno: pg_ident.conf line number (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, int lineno, IdentLine *ident,
+				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,11 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +494,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +516,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,8 +551,12 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-						tok_line->err_msg);
+		/* No error, set a new mapping number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->line_num, identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 68bb032d3e..e10d5124ab 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6131,15 +6131,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o}',
+  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 9dd137415e..6e29a6acc8 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,8 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1347,14 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v11-0002-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/plain; charset=us-asciiDownload
From 725eff02079fda19a7713620f0e436f9420f1130 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 11:15:06 +0800
Subject: [PATCH v11 2/3] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  22 +-
 src/backend/libpq/hba.c                       | 481 ++++++++++---
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  43 +-
 src/backend/utils/misc/guc-file.l             | 229 +++---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/include/libpq/hba.h                       |   5 +-
 src/include/utils/guc.h                       |   2 +
 .../authentication/t/003_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 12 files changed, 1310 insertions(+), 273 deletions(-)
 create mode 100644 src/test/authentication/t/003_file_inclusion.pl

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index c6f1b70fd3..42ceb6f3e6 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -835,8 +886,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -847,6 +900,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 4723f712a7..7d1cec8b7f 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1003,12 +1003,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1153,12 +1162,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this mapping
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_ident.conf</filename>
+       Line number of this mapping in the given <literal>file_name</literal>
       </para></entry>
      </row>
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 4637426d62..4e4a45b793 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -22,6 +22,7 @@
 #include <sys/param.h>
 #include <sys/socket.h>
 #include <netdb.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -69,6 +70,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -113,10 +120,22 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int depth, int elevel,
+							   char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   bool strict, const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -303,7 +322,7 @@ copy_auth_token(AuthToken *in)
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int depth, int elevel, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -319,7 +338,7 @@ next_field_expand(const char *filename, char **lineptr,
 
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
-			tokens = tokenize_inc_file(tokens, filename, buf + 1,
+			tokens = tokenize_inc_file(tokens, filename, buf + 1, depth + 1,
 									   elevel, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
@@ -347,6 +366,7 @@ static List *
 tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
+				  int depth,
 				  int elevel,
 				  char **err_msg)
 {
@@ -356,39 +376,30 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
 	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
+		*err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", *err_msg)));
+		return tokens;
 	}
 
-	inc_file = AllocateFile(inc_fullname, "r");
-	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, true,
+							 outer_filename, elevel, err_msg, &inc_fullname);
 
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
+	if (inc_file == NULL)
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, depth + 1,
+								 elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -426,11 +437,38 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -439,30 +477,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int depth, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -515,7 +545,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		{
 			List	   *current_field;
 
-			current_field = next_field_expand(filename, &lineptr,
+			current_field = next_field_expand(filename, &lineptr, depth,
 											  elevel, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
@@ -523,29 +553,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetDirConfFiles(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -855,7 +983,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -875,7 +1003,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -898,7 +1026,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry at end of line"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -911,7 +1039,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -934,6 +1062,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -948,6 +1077,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -962,7 +1092,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for connection type"),
 				 errhint("Specify exactly one connection type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for connection type";
 		return NULL;
 	}
@@ -990,7 +1120,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("hostssl record cannot match because SSL is disabled"),
 						 errhint("Set ssl = on in postgresql.conf."),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "hostssl record cannot match because SSL is disabled";
 			}
 #else
@@ -998,7 +1128,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostssl record cannot match because SSL is not supported by this build";
 #endif
 		}
@@ -1010,7 +1140,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
 #endif
 		}
@@ -1031,7 +1161,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid connection type \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid connection type \"%s\"", token->string);
 		return NULL;
 	}
@@ -1044,7 +1174,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before database specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before database specification";
 		return NULL;
 	}
@@ -1064,7 +1194,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before role specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before role specification";
 		return NULL;
 	}
@@ -1086,7 +1216,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("end-of-line before IP address specification"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "end-of-line before IP address specification";
 			return NULL;
 		}
@@ -1098,7 +1228,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("multiple values specified for host address"),
 					 errhint("Specify one address range per line."),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "multiple values specified for host address";
 			return NULL;
 		}
@@ -1157,7 +1287,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("invalid IP address \"%s\": %s",
 								str, gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("invalid IP address \"%s\": %s",
 									str, gai_strerror(ret));
 				if (gai_result)
@@ -1177,7 +1307,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
 										token->string);
 					return NULL;
@@ -1191,7 +1321,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid CIDR mask in address \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid CIDR mask in address \"%s\"",
 										token->string);
 					return NULL;
@@ -1211,7 +1341,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("end-of-line before netmask specification"),
 							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "end-of-line before netmask specification";
 					return NULL;
 				}
@@ -1222,7 +1352,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("multiple values specified for netmask"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "multiple values specified for netmask";
 					return NULL;
 				}
@@ -1237,7 +1367,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid IP mask \"%s\": %s",
 									token->string, gai_strerror(ret)),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid IP mask \"%s\": %s",
 										token->string, gai_strerror(ret));
 					if (gai_result)
@@ -1256,7 +1386,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("IP address and mask do not match"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "IP address and mask do not match";
 					return NULL;
 				}
@@ -1272,7 +1402,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before authentication method"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before authentication method";
 		return NULL;
 	}
@@ -1284,7 +1414,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for authentication type"),
 				 errhint("Specify exactly one authentication type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for authentication type";
 		return NULL;
 	}
@@ -1321,7 +1451,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
 			return NULL;
 		}
@@ -1362,7 +1492,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\"",
 							token->string);
 		return NULL;
@@ -1375,7 +1505,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\": not supported by this build",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
 							token->string);
 		return NULL;
@@ -1397,7 +1527,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("gssapi authentication is not supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "gssapi authentication is not supported on local sockets";
 		return NULL;
 	}
@@ -1409,7 +1539,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("peer authentication is only supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "peer authentication is only supported on local sockets";
 		return NULL;
 	}
@@ -1427,7 +1557,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("cert authentication is only supported on hostssl connections"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "cert authentication is only supported on hostssl connections";
 		return NULL;
 	}
@@ -1477,7 +1607,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("authentication option not in name=value format: %s", token->string),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("authentication option not in name=value format: %s",
 									token->string);
 				return NULL;
@@ -1521,7 +1651,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
 				return NULL;
 			}
@@ -1532,7 +1662,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(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)));
+								line_num, file_name)));
 			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
 			return NULL;
 		}
@@ -1548,7 +1678,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
 			return NULL;
 		}
@@ -1565,7 +1695,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS servers cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS servers cannot be empty";
 			return NULL;
 		}
@@ -1576,7 +1706,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS secrets cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS secrets cannot be empty";
 			return NULL;
 		}
@@ -1595,7 +1725,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiussecrets),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiussecrets),
 								list_length(parsedline->radiusservers));
@@ -1611,7 +1741,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusports),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusports),
 								list_length(parsedline->radiusservers));
@@ -1627,7 +1757,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusidentifiers),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusidentifiers),
 								list_length(parsedline->radiusservers));
@@ -1662,6 +1792,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -1685,7 +1816,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientcert can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1702,7 +1833,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
 				return false;
 			}
@@ -1715,7 +1846,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientcert: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1727,7 +1858,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientname can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientname can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1746,7 +1877,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientname: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1832,7 +1963,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid ldapscheme value: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 		hbaline->ldapscheme = pstrdup(val);
 	}
 	else if (strcmp(name, "ldapserver") == 0)
@@ -1850,7 +1981,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid LDAP port number: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid LDAP port number: \"%s\"", val);
 			return false;
 		}
@@ -1944,7 +2075,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS server list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -1963,7 +2094,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
 								(char *) lfirst(l), gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				if (gai_result)
 					pg_freeaddrinfo_all(hints.ai_family, gai_result);
 
@@ -1992,7 +2123,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS port list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
 			return false;
 		}
@@ -2005,7 +2136,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("invalid RADIUS port number: \"%s\"", val),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 
 				return false;
 			}
@@ -2028,7 +2159,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS secret list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2050,7 +2181,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS identifiers list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2064,7 +2195,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				 errmsg("unrecognized authentication option name: \"%s\"",
 						name),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
 							name);
 		return false;
@@ -2212,7 +2343,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2283,6 +2414,135 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename, bool strict,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		if (strict)
+		{
+			switch (kind)
+			{
+				case SecondaryAuthFile:
+					msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+					msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+					break;
+				case IncludedAuthFile:
+					msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+					msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+					break;
+				default:
+					elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+					break;
+			}
+
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg(msglog, inc_filename, *inc_fullname)));
+			*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+								strerror(save_errno));
+		}
+		else
+		{
+			Assert(kind == IncludedAuthFile);
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							*inc_fullname)));
+		}
+
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", err_msg)));
+		return err_msg;
+	}
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, strict,
+							 outer_filename, elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_inc_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+			return NULL;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, depth, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2301,6 +2561,7 @@ load_hba(void)
 IdentLine *
 parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
+	char	   *file_name = tok_line->file_name;
 	int			line_num = tok_line->line_num;
 	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
@@ -2361,7 +2622,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("invalid regular expression \"%s\": %s",
 							parsedline->ident_user + 1, errstr),
 					 errcontext("line %d of configuration file \"%s\"",
-							line_num, IdentFileName)));
+							line_num, file_name)));
 
 			*err_msg = psprintf("invalid regular expression \"%s\": %s",
 								parsedline->ident_user + 1, errstr);
@@ -2596,7 +2857,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index c9be4bff1f..15326a01e2 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int mapping_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int mapping_number, const char *filename,
+							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique rule identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -202,6 +203,8 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -345,7 +348,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -386,7 +389,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open configuration file \"%s\": %m",
 						HbaFileName)));
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -407,8 +410,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			rule_number++;
 
-		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -445,7 +448,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -454,7 +457,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * mapping_number: unique rule identifier among all valid rules
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -463,8 +467,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int mapping_number, int lineno, IdentLine *ident,
-				const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -482,6 +486,8 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -494,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -534,7 +540,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open usermap file \"%s\": %m",
 						IdentFileName)));
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -556,7 +562,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			mapping_number++;
 
 		fill_ident_line(tuple_store, tupdesc, mapping_number,
-						tok_line->line_num, identline, tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index 721628c0cf..86b6cc1c8a 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -345,6 +345,110 @@ GUC_flex_fatal(const char *msg)
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * Returns the list of config files located in a directory, in alphabetical
+ * order.
+ *
+ * We don't check for recursion or too-deep nesting depth here, its up to the
+ * caller to take care of that.
+ */
+char **
+GetDirConfFiles(const char *includedir, const char *calling_file, int elevel,
+				int *num_filenames, char **err_msg)
+{
+	char	   *directory;
+	DIR		   *d;
+	struct dirent *de;
+	char	  **filenames;
+	int			size_filenames;
+
+	/*
+	 * Reject directory name that is all-blank (including empty), as that
+	 * leads to confusion --- we'd read the containing directory, typically
+	 * resulting in recursive inclusion of the same file(s).
+	 */
+	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty configuration directory name: \"%s\"",
+						includedir)));
+		*err_msg = "empty configuration directory name";
+		return NULL;
+	}
+
+	directory = AbsoluteConfigLocation(includedir, calling_file);
+	d = AllocateDir(directory);
+	if (d == NULL)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration directory \"%s\": %m",
+						directory)));
+		*err_msg = psprintf("could not open directory \"%s\"", directory);
+		filenames = NULL;
+		goto cleanup;
+	}
+
+	/*
+	 * Read the directory and put the filenames in an array, so we can sort
+	 * them prior to caller processing the contents.
+	 */
+	size_filenames = 32;
+	filenames = (char **) palloc(size_filenames * sizeof(char *));
+	*num_filenames = 0;
+
+	while ((de = ReadDir(d, directory)) != NULL)
+	{
+		PGFileType	de_type;
+		char		filename[MAXPGPATH];
+
+		/*
+		 * Only parse files with names ending in ".conf".  Explicitly reject
+		 * files starting with ".".  This excludes things like "." and "..",
+		 * as well as typical hidden files, backup files, and editor debris.
+		 */
+		if (strlen(de->d_name) < 6)
+			continue;
+		if (de->d_name[0] == '.')
+			continue;
+		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+			continue;
+
+		join_path_components(filename, directory, de->d_name);
+		canonicalize_path(filename);
+		de_type = get_dirent_type(filename, de, true, elevel);
+		if (de_type == PGFILETYPE_ERROR)
+		{
+			*err_msg = psprintf("could not stat file \"%s\"", filename);
+			pfree(filenames);
+			filenames = NULL;
+			goto cleanup;
+		}
+		else if (de_type != PGFILETYPE_DIR)
+		{
+			/* Add file to array, increasing its size in blocks of 32 */
+			if (*num_filenames >= size_filenames)
+			{
+				size_filenames += 32;
+				filenames = (char **) repalloc(filenames,
+										size_filenames * sizeof(char *));
+			}
+			filenames[*num_filenames] = pstrdup(filename);
+			(*num_filenames)++;
+		}
+	}
+
+	if (*num_filenames > 0)
+		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
+
+cleanup:
+	if (d)
+		FreeDir(d);
+	pfree(directory);
+	return filenames;
+}
+
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -606,127 +710,30 @@ ParseConfigDirectory(const char *includedir,
 					 ConfigVariable **head_p,
 					 ConfigVariable **tail_p)
 {
-	char	   *directory;
-	DIR		   *d;
-	struct dirent *de;
+	char	   *err_msg;
 	char	  **filenames;
 	int			num_filenames;
-	int			size_filenames;
-	bool		status;
-
-	/*
-	 * Reject directory name that is all-blank (including empty), as that
-	 * leads to confusion --- we'd read the containing directory, typically
-	 * resulting in recursive inclusion of the same file(s).
-	 */
-	if (strspn(includedir, " \t\r\n") == strlen(includedir))
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("empty configuration directory name: \"%s\"",
-						includedir)));
-		record_config_file_error("empty configuration directory name",
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		return false;
-	}
-
-	/*
-	 * We don't check for recursion or too-deep nesting depth here; the
-	 * subsequent calls to ParseConfigFile will take care of that.
-	 */
-
-	directory = AbsoluteConfigLocation(includedir, calling_file);
-	d = AllocateDir(directory);
-	if (d == NULL)
-	{
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration directory \"%s\": %m",
-						directory)));
-		record_config_file_error(psprintf("could not open directory \"%s\"",
-										  directory),
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		status = false;
-		goto cleanup;
-	}
 
-	/*
-	 * Read the directory and put the filenames in an array, so we can sort
-	 * them prior to processing the contents.
-	 */
-	size_filenames = 32;
-	filenames = (char **) palloc(size_filenames * sizeof(char *));
-	num_filenames = 0;
+	filenames = GetDirConfFiles(includedir, calling_file, elevel,
+							   &num_filenames, &err_msg);
 
-	while ((de = ReadDir(d, directory)) != NULL)
+	if (!filenames)
 	{
-		PGFileType	de_type;
-		char		filename[MAXPGPATH];
-
-		/*
-		 * Only parse files with names ending in ".conf".  Explicitly reject
-		 * files starting with ".".  This excludes things like "." and "..",
-		 * as well as typical hidden files, backup files, and editor debris.
-		 */
-		if (strlen(de->d_name) < 6)
-			continue;
-		if (de->d_name[0] == '.')
-			continue;
-		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
-			continue;
-
-		join_path_components(filename, directory, de->d_name);
-		canonicalize_path(filename);
-		de_type = get_dirent_type(filename, de, true, elevel);
-		if (de_type == PGFILETYPE_ERROR)
-		{
-			record_config_file_error(psprintf("could not stat file \"%s\"",
-											  filename),
-									 calling_file, calling_lineno,
-									 head_p, tail_p);
-			status = false;
-			goto cleanup;
-		}
-		else if (de_type != PGFILETYPE_DIR)
-		{
-			/* Add file to array, increasing its size in blocks of 32 */
-			if (num_filenames >= size_filenames)
-			{
-				size_filenames += 32;
-				filenames = (char **) repalloc(filenames,
-											   size_filenames * sizeof(char *));
-			}
-			filenames[num_filenames] = pstrdup(filename);
-			num_filenames++;
-		}
+		record_config_file_error(err_msg, calling_file, calling_lineno, head_p,
+								 tail_p);
+		return false;
 	}
 
-	if (num_filenames > 0)
+	for (int i = 0; i < num_filenames; i++)
 	{
-		int			i;
-
-		qsort(filenames, num_filenames, sizeof(char *), pg_qsort_strcmp);
-		for (i = 0; i < num_filenames; i++)
-		{
-			if (!ParseConfigFile(filenames[i], true,
-								 calling_file, calling_lineno,
-								 depth, elevel,
-								 head_p, tail_p))
-			{
-				status = false;
-				goto cleanup;
-			}
-		}
+		if (!ParseConfigFile(filenames[i], true,
+							 calling_file, calling_lineno,
+							 depth, elevel,
+							 head_p, tail_p))
+			return false;
 	}
-	status = true;
 
-cleanup:
-	if (d)
-		FreeDir(d);
-	pfree(directory);
-	return status;
+	return true;
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index e10d5124ab..807fe3890b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6131,16 +6131,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index d06da81806..b45b5cff38 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -79,6 +79,7 @@ typedef enum ClientCertName
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -155,6 +156,7 @@ typedef struct AuthToken
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
@@ -174,6 +176,7 @@ extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int depth,
+										int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 1788361974..b465dc0356 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -144,6 +144,8 @@ typedef struct ConfigVariable
 	struct ConfigVariable *next;
 } ConfigVariable;
 
+extern char **GetDirConfFiles(const char *includedir, const char *calling_file,
+							  int elevel, int *num_filenames, char **err_msg);
 extern bool ParseConfigFile(const char *config_file, bool strict,
 							const char *calling_file, int calling_lineno,
 							int depth, int elevel,
diff --git a/src/test/authentication/t/003_file_inclusion.pl b/src/test/authentication/t/003_file_inclusion.pl
new file mode 100644
index 0000000000..8eae72b8d4
--- /dev/null
+++ b/src/test/authentication/t/003_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open secondary authentication file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open included authentication file "\.\.\/not_a_file" as "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open secondary authentication file \"\@dbnames_fails.conf\" as \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	'could not open configuration file "recurse.conf": maximum nesting depth exceeded');
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open secondary authentication file \"\@osnames_fails.conf\" as \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	'could not open configuration file "ident_recurse.conf": maximum nesting depth exceeded');
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT mapping_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY mapping_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 6e29a6acc8..ef17bf1e8a 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v11-0003-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From a674fddd83f6b4570c4b1b5c9537caac833fd213 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v11 3/3] POC: Add a pg_hba_matches() function.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 30a048f6b0..224b7a483a 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 4e4a45b793..c858f4188d 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -28,6 +28,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -43,6 +44,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -2967,3 +2969,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 807fe3890b..6274585c26 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6142,6 +6142,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.37.0

#51Julien Rouhaud
rjuju123@gmail.com
In reply to: Julien Rouhaud (#50)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Oct 10, 2022 at 10:51:32AM +0800, Julien Rouhaud wrote:

On Sun, Sep 18, 2022 at 01:06:12AM +0800, Julien Rouhaud wrote:

On Tue, Aug 16, 2022 at 02:10:30PM +0800, Julien Rouhaud wrote:

On Sat, Jul 30, 2022 at 04:09:36PM +0800, Julien Rouhaud wrote:

- 0001: the rule_number / mapping_number addition in the views in a separate
commit
- 0002: the main file inclusion patch. Only a few minor bugfix since
previous version discovered thanks to the tests (a bit more about it after),
and documentation tweaks based on previous discussions
- 0003: the pg_hba_matches() POC, no changes

v12 attached, fixing multiple conflicts with recent activity.

Attachments:

v12-0001-Add-rule_number-mapping_number-to-the-pg_hba-pg_.patchtext/plain; charset=us-asciiDownload
From 3361344bc0e907b341df912cf8ffcf28440b2370 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 10:59:51 +0800
Subject: [PATCH v12 1/3] Add rule_number / mapping_number to the
 pg_hba/pg_ident views.

Author: Julien Rouhaud
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/system-views.sgml      | 22 +++++++++++++
 src/backend/utils/adt/hbafuncs.c    | 50 ++++++++++++++++++++++-------
 src/include/catalog/pg_proc.dat     | 11 ++++---
 src/test/regress/expected/rules.out | 10 +++---
 4 files changed, 72 insertions(+), 21 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1ca7c3f9bf..4723f712a7 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -991,6 +991,18 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number of this rule among all rules if the rule is valid, otherwise
+       null. This indicates the order in which each rule will be considered
+       until the first matching one, if any, is used to perform authentication
+       with the client.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
@@ -1131,6 +1143,16 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Mapping number, in priority order, of this mapping if the mapping is
+       valid, otherwise null
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index cfdc4d8b39..21a451e391 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,10 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, int lineno, HbaLine *hba,
+						  const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int lineno, IdentLine *ident, const char *err_msg);
+							int mapping_number, int lineno, IdentLine *ident,
+							const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 10
 
 /*
  * fill_hba_line
@@ -165,6 +167,7 @@ get_hba_options(HbaLine *hba)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * rule_number: unique rule identifier among all valid rules
  * lineno: pg_hba.conf line number (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -174,7 +177,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +197,11 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +345,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +368,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,7 +403,11 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
+		/* No error, set a new rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
 					  hbaline, tok_line->err_msg);
 	}
 
@@ -430,8 +444,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -439,6 +453,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * mapping_number: unique rule identifier among all valid rules
  * lineno: pg_ident.conf line number (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, int lineno, IdentLine *ident,
+				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,11 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +494,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +516,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,8 +551,12 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-						tok_line->err_msg);
+		/* No error, set a new mapping number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->line_num, identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 62a5b8e655..4f5d05d0ce 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,15 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o}',
+  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index bfcd8ac9a0..178e536e21 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,8 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1347,14 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v12-0002-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/plain; charset=us-asciiDownload
From 6c4827443c4ec4f386f4f3519c52cebf3b57d6b9 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 11:15:06 +0800
Subject: [PATCH v12 2/3] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  22 +-
 src/backend/libpq/hba.c                       | 483 ++++++++++---
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  43 +-
 src/backend/utils/misc/guc-file.l             | 229 +++---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/include/libpq/hba.h                       |   5 +-
 src/include/utils/guc.h                       |   2 +
 .../authentication/t/003_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 12 files changed, 1311 insertions(+), 274 deletions(-)
 create mode 100644 src/test/authentication/t/003_file_inclusion.pl

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..2ae723de66 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -863,8 +914,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -875,6 +928,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 4723f712a7..7d1cec8b7f 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1003,12 +1003,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1153,12 +1162,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this mapping
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_ident.conf</filename>
+       Line number of this mapping in the given <literal>file_name</literal>
       </para></entry>
      </row>
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index ea92f02a47..183312b10a 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -22,6 +22,7 @@
 #include <sys/param.h>
 #include <sys/socket.h>
 #include <netdb.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -70,6 +71,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -115,14 +122,26 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int depth, int elevel,
+							   char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 							   char **err_msg, int elevel);
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   bool strict, const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -413,7 +432,7 @@ regexec_auth_token(const char *match, AuthToken *token, size_t nmatch,
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int depth, int elevel, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -429,7 +448,7 @@ next_field_expand(const char *filename, char **lineptr,
 
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
-			tokens = tokenize_inc_file(tokens, filename, buf + 1,
+			tokens = tokenize_inc_file(tokens, filename, buf + 1, depth + 1,
 									   elevel, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
@@ -457,6 +476,7 @@ static List *
 tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
+				  int depth,
 				  int elevel,
 				  char **err_msg)
 {
@@ -466,39 +486,30 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
 	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
+		*err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", *err_msg)));
+		return tokens;
 	}
 
-	inc_file = AllocateFile(inc_fullname, "r");
-	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, true,
+							 outer_filename, elevel, err_msg, &inc_fullname);
 
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
+	if (inc_file == NULL)
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, depth + 1,
+								 elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -536,11 +547,38 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -549,30 +587,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int depth, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -625,7 +655,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		{
 			List	   *current_field;
 
-			current_field = next_field_expand(filename, &lineptr,
+			current_field = next_field_expand(filename, &lineptr, depth,
 											  elevel, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
@@ -633,29 +663,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetDirConfFiles(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -984,7 +1112,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -1004,7 +1132,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -1027,7 +1155,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry at end of line"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -1040,7 +1168,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -1063,6 +1191,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -1077,6 +1206,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -1091,7 +1221,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for connection type"),
 				 errhint("Specify exactly one connection type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for connection type";
 		return NULL;
 	}
@@ -1119,7 +1249,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("hostssl record cannot match because SSL is disabled"),
 						 errhint("Set ssl = on in postgresql.conf."),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "hostssl record cannot match because SSL is disabled";
 			}
 #else
@@ -1127,7 +1257,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostssl record cannot match because SSL is not supported by this build";
 #endif
 		}
@@ -1139,7 +1269,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
 #endif
 		}
@@ -1160,7 +1290,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid connection type \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid connection type \"%s\"", token->string);
 		return NULL;
 	}
@@ -1173,7 +1303,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before database specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before database specification";
 		return NULL;
 	}
@@ -1198,7 +1328,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before role specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before role specification";
 		return NULL;
 	}
@@ -1225,7 +1355,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("end-of-line before IP address specification"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "end-of-line before IP address specification";
 			return NULL;
 		}
@@ -1237,7 +1367,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("multiple values specified for host address"),
 					 errhint("Specify one address range per line."),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "multiple values specified for host address";
 			return NULL;
 		}
@@ -1296,7 +1426,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("invalid IP address \"%s\": %s",
 								str, gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("invalid IP address \"%s\": %s",
 									str, gai_strerror(ret));
 				if (gai_result)
@@ -1316,7 +1446,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
 										token->string);
 					return NULL;
@@ -1330,7 +1460,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid CIDR mask in address \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid CIDR mask in address \"%s\"",
 										token->string);
 					return NULL;
@@ -1350,7 +1480,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("end-of-line before netmask specification"),
 							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "end-of-line before netmask specification";
 					return NULL;
 				}
@@ -1361,7 +1491,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("multiple values specified for netmask"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "multiple values specified for netmask";
 					return NULL;
 				}
@@ -1376,7 +1506,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid IP mask \"%s\": %s",
 									token->string, gai_strerror(ret)),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid IP mask \"%s\": %s",
 										token->string, gai_strerror(ret));
 					if (gai_result)
@@ -1395,7 +1525,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("IP address and mask do not match"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "IP address and mask do not match";
 					return NULL;
 				}
@@ -1411,7 +1541,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before authentication method"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before authentication method";
 		return NULL;
 	}
@@ -1423,7 +1553,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for authentication type"),
 				 errhint("Specify exactly one authentication type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for authentication type";
 		return NULL;
 	}
@@ -1460,7 +1590,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
 			return NULL;
 		}
@@ -1501,7 +1631,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\"",
 							token->string);
 		return NULL;
@@ -1514,7 +1644,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\": not supported by this build",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
 							token->string);
 		return NULL;
@@ -1536,7 +1666,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("gssapi authentication is not supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "gssapi authentication is not supported on local sockets";
 		return NULL;
 	}
@@ -1548,7 +1678,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("peer authentication is only supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "peer authentication is only supported on local sockets";
 		return NULL;
 	}
@@ -1566,7 +1696,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("cert authentication is only supported on hostssl connections"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "cert authentication is only supported on hostssl connections";
 		return NULL;
 	}
@@ -1616,7 +1746,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("authentication option not in name=value format: %s", token->string),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("authentication option not in name=value format: %s",
 									token->string);
 				return NULL;
@@ -1660,7 +1790,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
 				return NULL;
 			}
@@ -1671,7 +1801,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(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)));
+								line_num, file_name)));
 			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
 			return NULL;
 		}
@@ -1687,7 +1817,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
 			return NULL;
 		}
@@ -1704,7 +1834,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS servers cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS servers cannot be empty";
 			return NULL;
 		}
@@ -1715,7 +1845,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS secrets cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS secrets cannot be empty";
 			return NULL;
 		}
@@ -1734,7 +1864,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiussecrets),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiussecrets),
 								list_length(parsedline->radiusservers));
@@ -1750,7 +1880,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusports),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusports),
 								list_length(parsedline->radiusservers));
@@ -1766,7 +1896,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusidentifiers),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusidentifiers),
 								list_length(parsedline->radiusservers));
@@ -1801,6 +1931,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -1824,7 +1955,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientcert can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1841,7 +1972,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
 				return false;
 			}
@@ -1854,7 +1985,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientcert: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1866,7 +1997,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientname can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientname can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1885,7 +2016,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientname: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1971,7 +2102,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid ldapscheme value: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 		hbaline->ldapscheme = pstrdup(val);
 	}
 	else if (strcmp(name, "ldapserver") == 0)
@@ -1989,7 +2120,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid LDAP port number: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid LDAP port number: \"%s\"", val);
 			return false;
 		}
@@ -2083,7 +2214,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS server list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2102,7 +2233,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
 								(char *) lfirst(l), gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				if (gai_result)
 					pg_freeaddrinfo_all(hints.ai_family, gai_result);
 
@@ -2131,7 +2262,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS port list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
 			return false;
 		}
@@ -2144,7 +2275,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("invalid RADIUS port number: \"%s\"", val),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 
 				return false;
 			}
@@ -2167,7 +2298,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS secret list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2189,7 +2320,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS identifiers list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2203,7 +2334,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				 errmsg("unrecognized authentication option name: \"%s\"",
 						name),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
 							name);
 		return false;
@@ -2351,7 +2482,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2441,6 +2572,135 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename, bool strict,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		if (strict)
+		{
+			switch (kind)
+			{
+				case SecondaryAuthFile:
+					msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+					msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+					break;
+				case IncludedAuthFile:
+					msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+					msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+					break;
+				default:
+					elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+					break;
+			}
+
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg(msglog, inc_filename, *inc_fullname)));
+			*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+								strerror(save_errno));
+		}
+		else
+		{
+			Assert(kind == IncludedAuthFile);
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							*inc_fullname)));
+		}
+
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", err_msg)));
+		return err_msg;
+	}
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, strict,
+							 outer_filename, elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_inc_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+			return NULL;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, depth, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2459,6 +2719,7 @@ load_hba(void)
 IdentLine *
 parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
+	char	   *file_name = tok_line->file_name;
 	int			line_num = tok_line->line_num;
 	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
@@ -2500,8 +2761,8 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 	 * Now that the field validation is done, compile a regex from the user
 	 * token, if necessary.
 	 */
-	if (regcomp_auth_token(parsedline->token, IdentFileName, line_num,
-						   err_msg, elevel))
+	if (regcomp_auth_token(parsedline->token, file_name, line_num, err_msg,
+						   elevel))
 	{
 		/* err_msg includes the error to report */
 		return NULL;
@@ -2722,7 +2983,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 21a451e391..ea8de9a057 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int mapping_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int mapping_number, const char *filename,
+							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique rule identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -202,6 +203,8 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -345,7 +348,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -386,7 +389,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open configuration file \"%s\": %m",
 						HbaFileName)));
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -407,8 +410,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			rule_number++;
 
-		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -445,7 +448,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -454,7 +457,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * mapping_number: unique rule identifier among all valid rules
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -463,8 +467,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int mapping_number, int lineno, IdentLine *ident,
-				const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -482,6 +486,8 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -494,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -534,7 +540,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open usermap file \"%s\": %m",
 						IdentFileName)));
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -556,7 +562,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			mapping_number++;
 
 		fill_ident_line(tuple_store, tupdesc, mapping_number,
-						tok_line->line_num, identline, tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index 721628c0cf..86b6cc1c8a 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -345,6 +345,110 @@ GUC_flex_fatal(const char *msg)
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * Returns the list of config files located in a directory, in alphabetical
+ * order.
+ *
+ * We don't check for recursion or too-deep nesting depth here, its up to the
+ * caller to take care of that.
+ */
+char **
+GetDirConfFiles(const char *includedir, const char *calling_file, int elevel,
+				int *num_filenames, char **err_msg)
+{
+	char	   *directory;
+	DIR		   *d;
+	struct dirent *de;
+	char	  **filenames;
+	int			size_filenames;
+
+	/*
+	 * Reject directory name that is all-blank (including empty), as that
+	 * leads to confusion --- we'd read the containing directory, typically
+	 * resulting in recursive inclusion of the same file(s).
+	 */
+	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty configuration directory name: \"%s\"",
+						includedir)));
+		*err_msg = "empty configuration directory name";
+		return NULL;
+	}
+
+	directory = AbsoluteConfigLocation(includedir, calling_file);
+	d = AllocateDir(directory);
+	if (d == NULL)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration directory \"%s\": %m",
+						directory)));
+		*err_msg = psprintf("could not open directory \"%s\"", directory);
+		filenames = NULL;
+		goto cleanup;
+	}
+
+	/*
+	 * Read the directory and put the filenames in an array, so we can sort
+	 * them prior to caller processing the contents.
+	 */
+	size_filenames = 32;
+	filenames = (char **) palloc(size_filenames * sizeof(char *));
+	*num_filenames = 0;
+
+	while ((de = ReadDir(d, directory)) != NULL)
+	{
+		PGFileType	de_type;
+		char		filename[MAXPGPATH];
+
+		/*
+		 * Only parse files with names ending in ".conf".  Explicitly reject
+		 * files starting with ".".  This excludes things like "." and "..",
+		 * as well as typical hidden files, backup files, and editor debris.
+		 */
+		if (strlen(de->d_name) < 6)
+			continue;
+		if (de->d_name[0] == '.')
+			continue;
+		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+			continue;
+
+		join_path_components(filename, directory, de->d_name);
+		canonicalize_path(filename);
+		de_type = get_dirent_type(filename, de, true, elevel);
+		if (de_type == PGFILETYPE_ERROR)
+		{
+			*err_msg = psprintf("could not stat file \"%s\"", filename);
+			pfree(filenames);
+			filenames = NULL;
+			goto cleanup;
+		}
+		else if (de_type != PGFILETYPE_DIR)
+		{
+			/* Add file to array, increasing its size in blocks of 32 */
+			if (*num_filenames >= size_filenames)
+			{
+				size_filenames += 32;
+				filenames = (char **) repalloc(filenames,
+										size_filenames * sizeof(char *));
+			}
+			filenames[*num_filenames] = pstrdup(filename);
+			(*num_filenames)++;
+		}
+	}
+
+	if (*num_filenames > 0)
+		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
+
+cleanup:
+	if (d)
+		FreeDir(d);
+	pfree(directory);
+	return filenames;
+}
+
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -606,127 +710,30 @@ ParseConfigDirectory(const char *includedir,
 					 ConfigVariable **head_p,
 					 ConfigVariable **tail_p)
 {
-	char	   *directory;
-	DIR		   *d;
-	struct dirent *de;
+	char	   *err_msg;
 	char	  **filenames;
 	int			num_filenames;
-	int			size_filenames;
-	bool		status;
-
-	/*
-	 * Reject directory name that is all-blank (including empty), as that
-	 * leads to confusion --- we'd read the containing directory, typically
-	 * resulting in recursive inclusion of the same file(s).
-	 */
-	if (strspn(includedir, " \t\r\n") == strlen(includedir))
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("empty configuration directory name: \"%s\"",
-						includedir)));
-		record_config_file_error("empty configuration directory name",
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		return false;
-	}
-
-	/*
-	 * We don't check for recursion or too-deep nesting depth here; the
-	 * subsequent calls to ParseConfigFile will take care of that.
-	 */
-
-	directory = AbsoluteConfigLocation(includedir, calling_file);
-	d = AllocateDir(directory);
-	if (d == NULL)
-	{
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration directory \"%s\": %m",
-						directory)));
-		record_config_file_error(psprintf("could not open directory \"%s\"",
-										  directory),
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		status = false;
-		goto cleanup;
-	}
 
-	/*
-	 * Read the directory and put the filenames in an array, so we can sort
-	 * them prior to processing the contents.
-	 */
-	size_filenames = 32;
-	filenames = (char **) palloc(size_filenames * sizeof(char *));
-	num_filenames = 0;
+	filenames = GetDirConfFiles(includedir, calling_file, elevel,
+							   &num_filenames, &err_msg);
 
-	while ((de = ReadDir(d, directory)) != NULL)
+	if (!filenames)
 	{
-		PGFileType	de_type;
-		char		filename[MAXPGPATH];
-
-		/*
-		 * Only parse files with names ending in ".conf".  Explicitly reject
-		 * files starting with ".".  This excludes things like "." and "..",
-		 * as well as typical hidden files, backup files, and editor debris.
-		 */
-		if (strlen(de->d_name) < 6)
-			continue;
-		if (de->d_name[0] == '.')
-			continue;
-		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
-			continue;
-
-		join_path_components(filename, directory, de->d_name);
-		canonicalize_path(filename);
-		de_type = get_dirent_type(filename, de, true, elevel);
-		if (de_type == PGFILETYPE_ERROR)
-		{
-			record_config_file_error(psprintf("could not stat file \"%s\"",
-											  filename),
-									 calling_file, calling_lineno,
-									 head_p, tail_p);
-			status = false;
-			goto cleanup;
-		}
-		else if (de_type != PGFILETYPE_DIR)
-		{
-			/* Add file to array, increasing its size in blocks of 32 */
-			if (num_filenames >= size_filenames)
-			{
-				size_filenames += 32;
-				filenames = (char **) repalloc(filenames,
-											   size_filenames * sizeof(char *));
-			}
-			filenames[num_filenames] = pstrdup(filename);
-			num_filenames++;
-		}
+		record_config_file_error(err_msg, calling_file, calling_lineno, head_p,
+								 tail_p);
+		return false;
 	}
 
-	if (num_filenames > 0)
+	for (int i = 0; i < num_filenames; i++)
 	{
-		int			i;
-
-		qsort(filenames, num_filenames, sizeof(char *), pg_qsort_strcmp);
-		for (i = 0; i < num_filenames; i++)
-		{
-			if (!ParseConfigFile(filenames[i], true,
-								 calling_file, calling_lineno,
-								 depth, elevel,
-								 head_p, tail_p))
-			{
-				status = false;
-				goto cleanup;
-			}
-		}
+		if (!ParseConfigFile(filenames[i], true,
+							 calling_file, calling_lineno,
+							 depth, elevel,
+							 head_p, tail_p))
+			return false;
 	}
-	status = true;
 
-cleanup:
-	if (d)
-		FreeDir(d);
-	pfree(directory);
-	return status;
+	return true;
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4f5d05d0ce..2ad06c4d3e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,16 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index cec2e2665f..6b3bf814b5 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -93,6 +93,7 @@ typedef struct AuthToken
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -157,6 +158,7 @@ typedef struct IdentLine
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
@@ -176,6 +178,7 @@ extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int depth,
+										int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index b3aaff9665..59ca39d908 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -144,6 +144,8 @@ typedef struct ConfigVariable
 	struct ConfigVariable *next;
 } ConfigVariable;
 
+extern char **GetDirConfFiles(const char *includedir, const char *calling_file,
+							  int elevel, int *num_filenames, char **err_msg);
 extern bool ParseConfigFile(const char *config_file, bool strict,
 							const char *calling_file, int calling_lineno,
 							int depth, int elevel,
diff --git a/src/test/authentication/t/003_file_inclusion.pl b/src/test/authentication/t/003_file_inclusion.pl
new file mode 100644
index 0000000000..8eae72b8d4
--- /dev/null
+++ b/src/test/authentication/t/003_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open secondary authentication file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open included authentication file "\.\.\/not_a_file" as "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open secondary authentication file \"\@dbnames_fails.conf\" as \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	'could not open configuration file "recurse.conf": maximum nesting depth exceeded');
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open secondary authentication file \"\@osnames_fails.conf\" as \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	'could not open configuration file "ident_recurse.conf": maximum nesting depth exceeded');
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT mapping_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY mapping_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 178e536e21..3d8f182674 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v12-0003-POC-Add-a-pg_hba_matches-function.patchtext/plain; charset=us-asciiDownload
From e61beb1d2c40faa7a82517a3144b1324968ec3c3 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 22 Feb 2022 21:34:54 +0800
Subject: [PATCH v12 3/3] POC: Add a pg_hba_matches() function.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/catalog/system_functions.sql |   9 ++
 src/backend/libpq/hba.c                  | 138 +++++++++++++++++++++++
 src/include/catalog/pg_proc.dat          |   7 ++
 3 files changed, 154 insertions(+)

diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql
index 30a048f6b0..224b7a483a 100644
--- a/src/backend/catalog/system_functions.sql
+++ b/src/backend/catalog/system_functions.sql
@@ -594,6 +594,15 @@ LANGUAGE internal
 STRICT IMMUTABLE PARALLEL SAFE
 AS 'unicode_is_normalized';
 
+CREATE OR REPLACE FUNCTION
+  pg_hba_matches(
+    IN address inet, IN role text, IN ssl bool DEFAULT false,
+    OUT file_name text, OUT line_num int4, OUT raw_line text)
+RETURNS RECORD
+LANGUAGE INTERNAL
+VOLATILE
+AS 'pg_hba_matches';
+
 --
 -- The default permissions for functions mean that anyone can execute them.
 -- A number of functions shouldn't be executable by just anyone, but rather
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 183312b10a..1d64a984a8 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -28,6 +28,7 @@
 #include <unistd.h>
 
 #include "access/htup_details.h"
+#include "catalog/pg_authid.h"
 #include "catalog/pg_collation.h"
 #include "catalog/pg_type.h"
 #include "common/ip.h"
@@ -43,6 +44,7 @@
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
+#include "utils/inet.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
 #include "utils/varlena.h"
@@ -3091,3 +3093,139 @@ hba_authname(UserAuth auth_method)
 
 	return UserAuthName[auth_method];
 }
+
+#define PG_HBA_MATCHES_ATTS	3
+
+/*
+ * SQL-accessible SRF to return the entries that match the given connection
+ * info, if any.
+ */
+Datum pg_hba_matches(PG_FUNCTION_ARGS)
+{
+	MemoryContext ctxt;
+	inet	   *address = NULL;
+	bool		ssl_in_use = false;
+	hbaPort	   *port = palloc0(sizeof(hbaPort));
+	TupleDesc	tupdesc;
+	Datum		values[PG_HBA_MATCHES_ATTS];
+	bool		isnull[PG_HBA_MATCHES_ATTS];
+
+	if (!is_member_of_role(GetUserId(), ROLE_PG_READ_SERVER_FILES))
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("only superuser or a member of the pg_read_server_files role may call this function")));
+
+	if (PG_ARGISNULL(0))
+		port->raddr.addr.ss_family = AF_UNIX;
+	else
+	{
+		int			bits;
+		char	   *ptr;
+		char		tmp[sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:255.255.255.255/128")];
+
+		address = PG_GETARG_INET_PP(0);
+
+		bits = ip_maxbits(address) - ip_bits(address);
+		if (bits != 0)
+		{
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+					 errmsg("Invalid address")));
+		}
+
+		/* force display of max bits, regardless of masklen... */
+		if (pg_inet_net_ntop(ip_family(address), ip_addr(address),
+							 ip_maxbits(address), tmp, sizeof(tmp)) == NULL)
+			ereport(ERROR,
+					(errcode(ERRCODE_INVALID_BINARY_REPRESENTATION),
+					 errmsg("could not format inet value: %m")));
+
+		/* Suppress /n if present (shouldn't happen now) */
+		if ((ptr = strchr(tmp, '/')) != NULL)
+			*ptr = '\0';
+
+		switch (ip_family(address))
+		{
+			case PGSQL_AF_INET:
+			{
+				struct sockaddr_in *dst;
+
+				dst = (struct sockaddr_in *) &port->raddr.addr;
+				dst->sin_family = AF_INET;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin_addr, &ip_addr(address), sizeof(dst->sin_addr));
+
+				break;
+			}
+			/* See pg_inet_net_ntop() for details about those constants */
+			case PGSQL_AF_INET6:
+#if defined(AF_INET6) && AF_INET6 != PGSQL_AF_INET6
+			case AF_INET6:
+#endif
+			{
+				struct sockaddr_in6 *dst;
+
+				dst = (struct sockaddr_in6 *) &port->raddr.addr;
+				dst->sin6_family = AF_INET6;
+
+				/* ip_addr(address) always contains network representation */
+				memcpy(&dst->sin6_addr, &ip_addr(address), sizeof(dst->sin6_addr));
+
+				break;
+			}
+			default:
+				elog(ERROR, "unexpected ip_family: %d", ip_family(address));
+				break;
+		}
+	}
+
+	if (PG_ARGISNULL(1))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("parameter role is mandatory")));
+	port->user_name = text_to_cstring(PG_GETARG_TEXT_PP(1));
+
+	if (!PG_ARGISNULL(2))
+		ssl_in_use = PG_GETARG_BOOL(2);
+
+	port->ssl_in_use = ssl_in_use;
+
+	tupdesc = CreateTemplateTupleDesc(PG_HBA_MATCHES_ATTS);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 1, "file_name",
+					   TEXTOID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 2, "line_num",
+					   INT4OID, -1, 0);
+	TupleDescInitEntry(tupdesc, (AttrNumber) 3, "raw_line",
+					   TEXTOID, -1, 0);
+
+	BlessTupleDesc(tupdesc);
+
+	memset(isnull, 0, sizeof(isnull));
+
+	/* FIXME rework API to not rely on PostmasterContext */
+	ctxt = AllocSetContextCreate(CurrentMemoryContext, "load_hba",
+								 ALLOCSET_DEFAULT_SIZES);
+	PostmasterContext = AllocSetContextCreate(ctxt,
+											  "Postmaster",
+											  ALLOCSET_DEFAULT_SIZES);
+	parsed_hba_context = NULL;
+	if (!load_hba())
+		ereport(ERROR,
+				(errcode(ERRCODE_CONFIG_FILE_ERROR),
+				 errmsg("Invalidation auth configuration file")));
+
+	check_hba(port);
+
+	if (port->hba->auth_method == uaImplicitReject)
+		PG_RETURN_NULL();
+
+	values[0] = CStringGetTextDatum(port->hba->sourcefile);
+	values[1] = Int32GetDatum(port->hba->linenumber);
+	values[2] = CStringGetTextDatum(port->hba->rawline);
+
+	MemoryContextDelete(PostmasterContext);
+	PostmasterContext = NULL;
+
+	return HeapTupleGetDatum(heap_form_tuple(tupdesc, values, isnull));
+}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 2ad06c4d3e..60e313696d 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6146,6 +6146,13 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
+{ oid => '9557', descr => 'show wether the given connection would match an hba line',
+  proname => 'pg_hba_matches', provolatile => 'v', prorettype => 'record',
+  proargtypes => 'inet text bool', proisstrict => 'f',
+  proallargtypes => '{inet,text,bool,text,int4,text}',
+  proargmodes => '{i,i,i,o,o,o}',
+  proargnames => '{address,role,ssl,file_name,line_num,raw_line}',
+  prosrc => 'pg_hba_matches' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-- 
2.37.0

#52Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#51)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Oct 24, 2022 at 01:33:12PM +0800, Julien Rouhaud wrote:

v12 attached, fixing multiple conflicts with recent activity.

typedef struct TokenizedAuthLine
{
List *fields; /* List of lists of AuthTokens */
+ char *file_name; /* File name *

Hmm.  While putting my eyes on this patch set for the first time in a
few months (sorry!), the addition of file_name to TokenizedAuthLine in
0002 stands out.  This impacts all the macros used for the validation
of the ident and HBA lines, leading as well to a lot of bloat in the
patch with patterns like that in 20~25 places:
                 errcontext("line %d of configuration file \"%s\"", \
-                           line_num, IdentFileName))); \
+                           line_num, file_name))); \
[...]
                         errcontext("line %d of configuration file \"%s\"",
-                                   line_num, HbaFileName)));
+                                   line_num, file_name)));

Do you think that it would make sense to split that into its own
patch? That would move the code toward less references to HbaFileName
and IdentFileName, which is one step toward what we want for this
thread. This tokenization of HBA and ident files is already shared,
so moving this knowledge into TokenizedAuthLine looks helpful
independently of the rest.
--
Michael

#53Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#52)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Mon, Oct 24, 2022 at 04:13:51PM +0900, Michael Paquier wrote:

On Mon, Oct 24, 2022 at 01:33:12PM +0800, Julien Rouhaud wrote:

v12 attached, fixing multiple conflicts with recent activity.

typedef struct TokenizedAuthLine
{
List *fields; /* List of lists of AuthTokens */
+ char *file_name; /* File name *

Hmm.  While putting my eyes on this patch set for the first time in a
few months (sorry!), the addition of file_name to TokenizedAuthLine in
0002 stands out.  This impacts all the macros used for the validation
of the ident and HBA lines, leading as well to a lot of bloat in the
patch with patterns like that in 20~25 places:
errcontext("line %d of configuration file \"%s\"", \
-                           line_num, IdentFileName))); \
+                           line_num, file_name))); \
[...]
errcontext("line %d of configuration file \"%s\"",
-                                   line_num, HbaFileName)));
+                                   line_num, file_name)));

Do you think that it would make sense to split that into its own
patch? That would move the code toward less references to HbaFileName
and IdentFileName, which is one step toward what we want for this
thread. This tokenization of HBA and ident files is already shared,
so moving this knowledge into TokenizedAuthLine looks helpful
independently of the rest.

It would also require to bring HbaLine->sourcefile. I'm afraid it would be
weird to introduce such a refactoring in a separate commit just to pass a
constant down multiple level of indirection, as all the macro will remain
specific to either hba or ident anyway.

I agree that there are quite a lot of s/XXXFileName/file_name/, but those
aren't complicated, and keeping them in the same commit makes it easy to
validate that none has been forgotten since the regression tests covering those
messages are in that commit too.

And of course while double checking that none was forgotten I realize that I
missed the new regcomp_auth_token() which introduced a couple new usage of
HbaFileName.

#54Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#53)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Oct 24, 2022 at 04:03:03PM +0800, Julien Rouhaud wrote:

It would also require to bring HbaLine->sourcefile. I'm afraid it would be
weird to introduce such a refactoring in a separate commit just to pass a
constant down multiple level of indirection, as all the macro will remain
specific to either hba or ident anyway.

Putting my hands on it, I am not really afraid of doing that
independently. From what I can see, this is cutting 23kB worth of
diffs from 0002, reducing it from 94K to 71kB.

I agree that there are quite a lot of s/XXXFileName/file_name/, but those
aren't complicated, and keeping them in the same commit makes it easy to
validate that none has been forgotten since the regression tests covering those
messages are in that commit too.

Another advantage is that it minimizes the presence of the hardcoded
HbaFileName and IdentFileName in hba.c, which is one thing we are
trying to achieve here for the inclusion of more files. I found a bit
strange that IdentLine had no sourcefile, actually. We track the file
number but use it nowhere, and it seems to me that having more
symmetry between both would be a good thing.

So, the key of the logic is how we are going to organize the
tokenization of the HBA and ident lines through all the inclusions..
As far as I get it, tokenize_auth_file() is the root call and
tokenize_file_with_context() with its depth is able to work on each
individual file, and it can optionally recurse depending on what's
included. Why do you need to switch to the old context in
tokenize_file_with_context()? Could it be simpler to switch once to
linecxt outside of the internal routine?

It looks like GetDirConfFiles() is another piece that can be
refactored and reviewed on its own, as we use it in
ParseConfigDirectory()@guc.c.
--
Michael

Attachments:

v13-0001-Refactor-knowledge-of-origin-file-in-hba.c.patchtext/x-diff; charset=us-asciiDownload
From 7c12a9dd2b23765c0e0d38da8140051a89c45fb4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 25 Oct 2022 15:17:27 +0900
Subject: [PATCH v13 1/3] Refactor knowledge of origin file in hba.c

This limits the footprint of HbaFileName and IdentFileName to their
entry loading point, easing the introduction of the inclusion logic.
---
 src/include/libpq/hba.h |   3 ++
 src/backend/libpq/hba.c | 114 +++++++++++++++++++++-------------------
 2 files changed, 63 insertions(+), 54 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index cec2e2665f..bf896ac084 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -93,6 +93,7 @@ typedef struct AuthToken
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -138,6 +139,7 @@ typedef struct HbaLine
 
 typedef struct IdentLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 
 	char	   *usermap;
@@ -157,6 +159,7 @@ typedef struct IdentLine
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name of origin */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index ea92f02a47..6524b60610 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -641,6 +641,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 
 			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
 			tok_line->fields = current_line;
+			tok_line->file_name = pstrdup(filename);
 			tok_line->line_num = line_number;
 			tok_line->raw_line = pstrdup(buf.data);
 			tok_line->err_msg = err_msg;
@@ -984,7 +985,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -1004,7 +1005,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -1027,7 +1028,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry at end of line"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -1040,7 +1041,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -1063,6 +1064,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -1077,6 +1079,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(tok_line->file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -1091,7 +1094,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for connection type"),
 				 errhint("Specify exactly one connection type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for connection type";
 		return NULL;
 	}
@@ -1119,7 +1122,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("hostssl record cannot match because SSL is disabled"),
 						 errhint("Set ssl = on in postgresql.conf."),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "hostssl record cannot match because SSL is disabled";
 			}
 #else
@@ -1127,7 +1130,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostssl record cannot match because SSL is not supported by this build";
 #endif
 		}
@@ -1139,7 +1142,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
 #endif
 		}
@@ -1160,7 +1163,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid connection type \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid connection type \"%s\"", token->string);
 		return NULL;
 	}
@@ -1173,7 +1176,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before database specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before database specification";
 		return NULL;
 	}
@@ -1184,7 +1187,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 		AuthToken  *tok = copy_auth_token(lfirst(tokencell));
 
 		/* Compile a regexp for the database token, if necessary */
-		if (regcomp_auth_token(tok, HbaFileName, line_num, err_msg, elevel))
+		if (regcomp_auth_token(tok, file_name, line_num, err_msg, elevel))
 			return NULL;
 
 		parsedline->databases = lappend(parsedline->databases, tok);
@@ -1198,7 +1201,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before role specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before role specification";
 		return NULL;
 	}
@@ -1209,7 +1212,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 		AuthToken  *tok = copy_auth_token(lfirst(tokencell));
 
 		/* Compile a regexp from the role token, if necessary */
-		if (regcomp_auth_token(tok, HbaFileName, line_num, err_msg, elevel))
+		if (regcomp_auth_token(tok, file_name, line_num, err_msg, elevel))
 			return NULL;
 
 		parsedline->roles = lappend(parsedline->roles, tok);
@@ -1225,7 +1228,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("end-of-line before IP address specification"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "end-of-line before IP address specification";
 			return NULL;
 		}
@@ -1237,7 +1240,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("multiple values specified for host address"),
 					 errhint("Specify one address range per line."),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "multiple values specified for host address";
 			return NULL;
 		}
@@ -1296,7 +1299,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("invalid IP address \"%s\": %s",
 								str, gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("invalid IP address \"%s\": %s",
 									str, gai_strerror(ret));
 				if (gai_result)
@@ -1316,7 +1319,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
 										token->string);
 					return NULL;
@@ -1330,7 +1333,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid CIDR mask in address \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid CIDR mask in address \"%s\"",
 										token->string);
 					return NULL;
@@ -1350,7 +1353,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("end-of-line before netmask specification"),
 							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "end-of-line before netmask specification";
 					return NULL;
 				}
@@ -1361,7 +1364,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("multiple values specified for netmask"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "multiple values specified for netmask";
 					return NULL;
 				}
@@ -1376,7 +1379,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid IP mask \"%s\": %s",
 									token->string, gai_strerror(ret)),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid IP mask \"%s\": %s",
 										token->string, gai_strerror(ret));
 					if (gai_result)
@@ -1395,7 +1398,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("IP address and mask do not match"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "IP address and mask do not match";
 					return NULL;
 				}
@@ -1411,7 +1414,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before authentication method"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before authentication method";
 		return NULL;
 	}
@@ -1423,7 +1426,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for authentication type"),
 				 errhint("Specify exactly one authentication type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for authentication type";
 		return NULL;
 	}
@@ -1460,7 +1463,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
 			return NULL;
 		}
@@ -1501,7 +1504,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\"",
 							token->string);
 		return NULL;
@@ -1514,7 +1517,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\": not supported by this build",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
 							token->string);
 		return NULL;
@@ -1536,7 +1539,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("gssapi authentication is not supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "gssapi authentication is not supported on local sockets";
 		return NULL;
 	}
@@ -1548,7 +1551,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("peer authentication is only supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "peer authentication is only supported on local sockets";
 		return NULL;
 	}
@@ -1566,7 +1569,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("cert authentication is only supported on hostssl connections"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "cert authentication is only supported on hostssl connections";
 		return NULL;
 	}
@@ -1616,7 +1619,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("authentication option not in name=value format: %s", token->string),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("authentication option not in name=value format: %s",
 									token->string);
 				return NULL;
@@ -1660,7 +1663,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
 				return NULL;
 			}
@@ -1671,7 +1674,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(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)));
+								line_num, file_name)));
 			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
 			return NULL;
 		}
@@ -1687,7 +1690,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
 			return NULL;
 		}
@@ -1704,7 +1707,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS servers cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS servers cannot be empty";
 			return NULL;
 		}
@@ -1715,7 +1718,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS secrets cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS secrets cannot be empty";
 			return NULL;
 		}
@@ -1734,7 +1737,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiussecrets),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiussecrets),
 								list_length(parsedline->radiusservers));
@@ -1750,7 +1753,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusports),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusports),
 								list_length(parsedline->radiusservers));
@@ -1766,7 +1769,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusidentifiers),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusidentifiers),
 								list_length(parsedline->radiusservers));
@@ -1801,6 +1804,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -1824,7 +1828,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientcert can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1841,7 +1845,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
 				return false;
 			}
@@ -1854,7 +1858,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientcert: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1866,7 +1870,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientname can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientname can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1885,7 +1889,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientname: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1971,7 +1975,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid ldapscheme value: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 		hbaline->ldapscheme = pstrdup(val);
 	}
 	else if (strcmp(name, "ldapserver") == 0)
@@ -1989,7 +1993,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid LDAP port number: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid LDAP port number: \"%s\"", val);
 			return false;
 		}
@@ -2083,7 +2087,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS server list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2102,7 +2106,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
 								(char *) lfirst(l), gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				if (gai_result)
 					pg_freeaddrinfo_all(hints.ai_family, gai_result);
 
@@ -2131,7 +2135,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS port list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
 			return false;
 		}
@@ -2144,7 +2148,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("invalid RADIUS port number: \"%s\"", val),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 
 				return false;
 			}
@@ -2167,7 +2171,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS secret list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2189,7 +2193,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS identifiers list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2203,7 +2207,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				 errmsg("unrecognized authentication option name: \"%s\"",
 						name),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
 							name);
 		return false;
@@ -2460,6 +2464,7 @@ IdentLine *
 parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
 	List	   *tokens;
@@ -2471,6 +2476,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 
 	parsedline = palloc0(sizeof(IdentLine));
 	parsedline->linenumber = line_num;
+	parsedline->sourcefile = pstrdup(tok_line->file_name);
 
 	/* Get the map token (must exist) */
 	tokens = lfirst(field);
@@ -2500,7 +2506,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 	 * Now that the field validation is done, compile a regex from the user
 	 * token, if necessary.
 	 */
-	if (regcomp_auth_token(parsedline->token, IdentFileName, line_num,
+	if (regcomp_auth_token(parsedline->token, file_name, line_num,
 						   err_msg, elevel))
 	{
 		/* err_msg includes the error to report */
-- 
2.37.2

v13-0002-Add-rule_number-mapping_number-to-the-pg_hba-pg_.patchtext/x-diff; charset=us-asciiDownload
From 5f46ec8496f5d3e99ba72867feb1fd019e0e9497 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 10:59:51 +0800
Subject: [PATCH v13 2/3] Add rule_number / mapping_number to the
 pg_hba/pg_ident views.

Author: Julien Rouhaud
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat     | 11 ++++---
 src/backend/utils/adt/hbafuncs.c    | 50 ++++++++++++++++++++++-------
 src/test/regress/expected/rules.out | 10 +++---
 doc/src/sgml/system-views.sgml      | 22 +++++++++++++
 4 files changed, 72 insertions(+), 21 deletions(-)

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 62a5b8e655..4f5d05d0ce 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,15 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o}',
+  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index cfdc4d8b39..21a451e391 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,10 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  int rule_number, int lineno, HbaLine *hba,
+						  const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int lineno, IdentLine *ident, const char *err_msg);
+							int mapping_number, int lineno, IdentLine *ident,
+							const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -157,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 10
 
 /*
  * fill_hba_line
@@ -165,6 +167,7 @@ get_hba_options(HbaLine *hba)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * rule_number: unique rule identifier among all valid rules
  * lineno: pg_hba.conf line number (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -174,7 +177,8 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			  int rule_number, int lineno, HbaLine *hba,
+			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -193,6 +197,11 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -336,7 +345,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -359,6 +368,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -393,7 +403,11 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
+		/* No error, set a new rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
 					  hbaline, tok_line->err_msg);
 	}
 
@@ -430,8 +444,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -439,6 +453,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * mapping_number: unique rule identifier among all valid rules
  * lineno: pg_ident.conf line number (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
@@ -448,7 +463,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				int mapping_number, int lineno, IdentLine *ident,
+				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -461,6 +477,11 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -473,7 +494,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -495,6 +516,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -529,8 +551,12 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-						tok_line->err_msg);
+		/* No error, set a new mapping number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						tok_line->line_num, identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index bfcd8ac9a0..178e536e21 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,8 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1347,14 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1ca7c3f9bf..4723f712a7 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -991,6 +991,18 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number of this rule among all rules if the rule is valid, otherwise
+       null. This indicates the order in which each rule will be considered
+       until the first matching one, if any, is used to perform authentication
+       with the client.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
@@ -1131,6 +1143,16 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Mapping number, in priority order, of this mapping if the mapping is
+       valid, otherwise null
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
-- 
2.37.2

v13-0003-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/x-diff; charset=us-asciiDownload
From fdf05163bcad4266aef5f46f7a9c6e9253d8febc Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 25 Oct 2022 15:26:50 +0900
Subject: [PATCH v13 3/3] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/include/libpq/hba.h                       |   3 +-
 src/include/utils/guc.h                       |   2 +
 src/backend/libpq/hba.c                       | 378 ++++++++--
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  43 +-
 src/backend/utils/misc/guc-file.l             | 229 +++---
 .../authentication/t/003_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  22 +-
 12 files changed, 1254 insertions(+), 224 deletions(-)
 create mode 100644 src/test/authentication/t/003_file_inclusion.pl

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4f5d05d0ce..2ad06c4d3e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,16 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index bf896ac084..7108cd2dae 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -179,6 +179,7 @@ extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int depth,
+										int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index b3aaff9665..59ca39d908 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -144,6 +144,8 @@ typedef struct ConfigVariable
 	struct ConfigVariable *next;
 } ConfigVariable;
 
+extern char **GetDirConfFiles(const char *includedir, const char *calling_file,
+							  int elevel, int *num_filenames, char **err_msg);
 extern bool ParseConfigFile(const char *config_file, bool strict,
 							const char *calling_file, int calling_lineno,
 							int depth, int elevel,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 6524b60610..21e8015ac8 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -22,6 +22,7 @@
 #include <sys/param.h>
 #include <sys/socket.h>
 #include <netdb.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -70,6 +71,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -115,14 +122,26 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int depth, int elevel,
+							   char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 							   char **err_msg, int elevel);
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   bool strict, const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -413,7 +432,7 @@ regexec_auth_token(const char *match, AuthToken *token, size_t nmatch,
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int depth, int elevel, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -429,7 +448,7 @@ next_field_expand(const char *filename, char **lineptr,
 
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
-			tokens = tokenize_inc_file(tokens, filename, buf + 1,
+			tokens = tokenize_inc_file(tokens, filename, buf + 1, depth + 1,
 									   elevel, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
@@ -457,6 +476,7 @@ static List *
 tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
+				  int depth,
 				  int elevel,
 				  char **err_msg)
 {
@@ -466,39 +486,30 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
 	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
-
-	inc_file = AllocateFile(inc_fullname, "r");
-	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
-
+		*err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
 		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", *err_msg)));
 		return tokens;
 	}
 
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, true,
+							 outer_filename, elevel, err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+		return tokens;
+
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, depth + 1,
+								 elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -536,11 +547,38 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -549,30 +587,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int depth, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -625,7 +655,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		{
 			List	   *current_field;
 
-			current_field = next_field_expand(filename, &lineptr,
+			current_field = next_field_expand(filename, &lineptr, depth,
 											  elevel, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
@@ -633,30 +663,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedAuthLine *tok_line;
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->file_name = pstrdup(filename);
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetDirConfFiles(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -2355,7 +2482,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2445,6 +2572,135 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename, bool strict,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		if (strict)
+		{
+			switch (kind)
+			{
+				case SecondaryAuthFile:
+					msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+					msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+					break;
+				case IncludedAuthFile:
+					msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+					msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+					break;
+				default:
+					elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+					break;
+			}
+
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg(msglog, inc_filename, *inc_fullname)));
+			*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+								strerror(save_errno));
+		}
+		else
+		{
+			Assert(kind == IncludedAuthFile);
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							*inc_fullname)));
+		}
+
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", err_msg)));
+		return err_msg;
+	}
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, strict,
+							 outer_filename, elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_inc_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+			return NULL;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, depth, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2728,7 +2984,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 21a451e391..ea8de9a057 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, const char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int mapping_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int mapping_number, const char *filename,
+							int lineno, IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique rule identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, const char *filename, int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -202,6 +203,8 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -345,7 +348,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -386,7 +389,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open configuration file \"%s\": %m",
 						HbaFileName)));
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -407,8 +410,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			rule_number++;
 
-		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		fill_hba_line(tuple_store, tupdesc, rule_number, tok_line->file_name,
+					  tok_line->line_num, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -445,7 +448,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -454,7 +457,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * mapping_number: unique rule identifier among all valid rules
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: name of the file containing that line
+ * lineno: line number in that file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -463,8 +467,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int mapping_number, int lineno, IdentLine *ident,
-				const char *err_msg)
+				int mapping_number, const char *filename, int lineno,
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -482,6 +486,8 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -494,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -534,7 +540,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open usermap file \"%s\": %m",
 						IdentFileName)));
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -556,7 +562,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			mapping_number++;
 
 		fill_ident_line(tuple_store, tupdesc, mapping_number,
-						tok_line->line_num, identline, tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index 721628c0cf..86b6cc1c8a 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -345,6 +345,110 @@ GUC_flex_fatal(const char *msg)
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * Returns the list of config files located in a directory, in alphabetical
+ * order.
+ *
+ * We don't check for recursion or too-deep nesting depth here, its up to the
+ * caller to take care of that.
+ */
+char **
+GetDirConfFiles(const char *includedir, const char *calling_file, int elevel,
+				int *num_filenames, char **err_msg)
+{
+	char	   *directory;
+	DIR		   *d;
+	struct dirent *de;
+	char	  **filenames;
+	int			size_filenames;
+
+	/*
+	 * Reject directory name that is all-blank (including empty), as that
+	 * leads to confusion --- we'd read the containing directory, typically
+	 * resulting in recursive inclusion of the same file(s).
+	 */
+	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty configuration directory name: \"%s\"",
+						includedir)));
+		*err_msg = "empty configuration directory name";
+		return NULL;
+	}
+
+	directory = AbsoluteConfigLocation(includedir, calling_file);
+	d = AllocateDir(directory);
+	if (d == NULL)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration directory \"%s\": %m",
+						directory)));
+		*err_msg = psprintf("could not open directory \"%s\"", directory);
+		filenames = NULL;
+		goto cleanup;
+	}
+
+	/*
+	 * Read the directory and put the filenames in an array, so we can sort
+	 * them prior to caller processing the contents.
+	 */
+	size_filenames = 32;
+	filenames = (char **) palloc(size_filenames * sizeof(char *));
+	*num_filenames = 0;
+
+	while ((de = ReadDir(d, directory)) != NULL)
+	{
+		PGFileType	de_type;
+		char		filename[MAXPGPATH];
+
+		/*
+		 * Only parse files with names ending in ".conf".  Explicitly reject
+		 * files starting with ".".  This excludes things like "." and "..",
+		 * as well as typical hidden files, backup files, and editor debris.
+		 */
+		if (strlen(de->d_name) < 6)
+			continue;
+		if (de->d_name[0] == '.')
+			continue;
+		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+			continue;
+
+		join_path_components(filename, directory, de->d_name);
+		canonicalize_path(filename);
+		de_type = get_dirent_type(filename, de, true, elevel);
+		if (de_type == PGFILETYPE_ERROR)
+		{
+			*err_msg = psprintf("could not stat file \"%s\"", filename);
+			pfree(filenames);
+			filenames = NULL;
+			goto cleanup;
+		}
+		else if (de_type != PGFILETYPE_DIR)
+		{
+			/* Add file to array, increasing its size in blocks of 32 */
+			if (*num_filenames >= size_filenames)
+			{
+				size_filenames += 32;
+				filenames = (char **) repalloc(filenames,
+										size_filenames * sizeof(char *));
+			}
+			filenames[*num_filenames] = pstrdup(filename);
+			(*num_filenames)++;
+		}
+	}
+
+	if (*num_filenames > 0)
+		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
+
+cleanup:
+	if (d)
+		FreeDir(d);
+	pfree(directory);
+	return filenames;
+}
+
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -606,127 +710,30 @@ ParseConfigDirectory(const char *includedir,
 					 ConfigVariable **head_p,
 					 ConfigVariable **tail_p)
 {
-	char	   *directory;
-	DIR		   *d;
-	struct dirent *de;
+	char	   *err_msg;
 	char	  **filenames;
 	int			num_filenames;
-	int			size_filenames;
-	bool		status;
 
-	/*
-	 * Reject directory name that is all-blank (including empty), as that
-	 * leads to confusion --- we'd read the containing directory, typically
-	 * resulting in recursive inclusion of the same file(s).
-	 */
-	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	filenames = GetDirConfFiles(includedir, calling_file, elevel,
+							   &num_filenames, &err_msg);
+
+	if (!filenames)
 	{
-		ereport(elevel,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("empty configuration directory name: \"%s\"",
-						includedir)));
-		record_config_file_error("empty configuration directory name",
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
+		record_config_file_error(err_msg, calling_file, calling_lineno, head_p,
+								 tail_p);
 		return false;
 	}
 
-	/*
-	 * We don't check for recursion or too-deep nesting depth here; the
-	 * subsequent calls to ParseConfigFile will take care of that.
-	 */
-
-	directory = AbsoluteConfigLocation(includedir, calling_file);
-	d = AllocateDir(directory);
-	if (d == NULL)
+	for (int i = 0; i < num_filenames; i++)
 	{
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration directory \"%s\": %m",
-						directory)));
-		record_config_file_error(psprintf("could not open directory \"%s\"",
-										  directory),
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		status = false;
-		goto cleanup;
+		if (!ParseConfigFile(filenames[i], true,
+							 calling_file, calling_lineno,
+							 depth, elevel,
+							 head_p, tail_p))
+			return false;
 	}
 
-	/*
-	 * Read the directory and put the filenames in an array, so we can sort
-	 * them prior to processing the contents.
-	 */
-	size_filenames = 32;
-	filenames = (char **) palloc(size_filenames * sizeof(char *));
-	num_filenames = 0;
-
-	while ((de = ReadDir(d, directory)) != NULL)
-	{
-		PGFileType	de_type;
-		char		filename[MAXPGPATH];
-
-		/*
-		 * Only parse files with names ending in ".conf".  Explicitly reject
-		 * files starting with ".".  This excludes things like "." and "..",
-		 * as well as typical hidden files, backup files, and editor debris.
-		 */
-		if (strlen(de->d_name) < 6)
-			continue;
-		if (de->d_name[0] == '.')
-			continue;
-		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
-			continue;
-
-		join_path_components(filename, directory, de->d_name);
-		canonicalize_path(filename);
-		de_type = get_dirent_type(filename, de, true, elevel);
-		if (de_type == PGFILETYPE_ERROR)
-		{
-			record_config_file_error(psprintf("could not stat file \"%s\"",
-											  filename),
-									 calling_file, calling_lineno,
-									 head_p, tail_p);
-			status = false;
-			goto cleanup;
-		}
-		else if (de_type != PGFILETYPE_DIR)
-		{
-			/* Add file to array, increasing its size in blocks of 32 */
-			if (num_filenames >= size_filenames)
-			{
-				size_filenames += 32;
-				filenames = (char **) repalloc(filenames,
-											   size_filenames * sizeof(char *));
-			}
-			filenames[num_filenames] = pstrdup(filename);
-			num_filenames++;
-		}
-	}
-
-	if (num_filenames > 0)
-	{
-		int			i;
-
-		qsort(filenames, num_filenames, sizeof(char *), pg_qsort_strcmp);
-		for (i = 0; i < num_filenames; i++)
-		{
-			if (!ParseConfigFile(filenames[i], true,
-								 calling_file, calling_lineno,
-								 depth, elevel,
-								 head_p, tail_p))
-			{
-				status = false;
-				goto cleanup;
-			}
-		}
-	}
-	status = true;
-
-cleanup:
-	if (d)
-		FreeDir(d);
-	pfree(directory);
-	return status;
+	return true;
 }
 
 /*
diff --git a/src/test/authentication/t/003_file_inclusion.pl b/src/test/authentication/t/003_file_inclusion.pl
new file mode 100644
index 0000000000..8eae72b8d4
--- /dev/null
+++ b/src/test/authentication/t/003_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open secondary authentication file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open included authentication file "\.\.\/not_a_file" as "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open secondary authentication file \"\@dbnames_fails.conf\" as \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	'could not open configuration file "recurse.conf": maximum nesting depth exceeded');
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open secondary authentication file \"\@osnames_fails.conf\" as \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	'could not open configuration file "ident_recurse.conf": maximum nesting depth exceeded');
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT mapping_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY mapping_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 178e536e21..3d8f182674 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..2ae723de66 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -863,8 +914,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -875,6 +928,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 4723f712a7..7d1cec8b7f 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1003,12 +1003,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1153,12 +1162,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this mapping
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_ident.conf</filename>
+       Line number of this mapping in the given <literal>file_name</literal>
       </para></entry>
      </row>
 
-- 
2.37.2

#55Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#54)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Oct 25, 2022 at 03:43:21PM +0900, Michael Paquier wrote:

Another advantage is that it minimizes the presence of the hardcoded
HbaFileName and IdentFileName in hba.c, which is one thing we are
trying to achieve here for the inclusion of more files. I found a bit
strange that IdentLine had no sourcefile, actually. We track the file
number but use it nowhere, and it seems to me that having more
symmetry between both would be a good thing.

If IdentLine->linenumber is useless, why not get rid of it rather than tracking
another useless info? Hba is much more structured so we need a more
specialized struct, but I don't think ident will ever go that way.

So, the key of the logic is how we are going to organize the
tokenization of the HBA and ident lines through all the inclusions..
As far as I get it, tokenize_auth_file() is the root call and
tokenize_file_with_context() with its depth is able to work on each
individual file, and it can optionally recurse depending on what's
included. Why do you need to switch to the old context in
tokenize_file_with_context()? Could it be simpler to switch once to
linecxt outside of the internal routine?

It just seemed the cleanest way to go. We could do without but then we would
have to duplicate MemoryContextSwitchTo calls all over the place, and probably
handling an optional memory context creation in the function.

It looks like GetDirConfFiles() is another piece that can be
refactored and reviewed on its own, as we use it in
ParseConfigDirectory()@guc.c.

I'm fine with it.

#56Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#55)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Oct 25, 2022 at 03:08:59PM +0800, Julien Rouhaud wrote:

On Tue, Oct 25, 2022 at 03:43:21PM +0900, Michael Paquier wrote:

Another advantage is that it minimizes the presence of the hardcoded
HbaFileName and IdentFileName in hba.c, which is one thing we are
trying to achieve here for the inclusion of more files. I found a bit
strange that IdentLine had no sourcefile, actually. We track the file
number but use it nowhere, and it seems to me that having more
symmetry between both would be a good thing.

If IdentLine->linenumber is useless, why not get rid of it rather than tracking
another useless info? Hba is much more structured so we need a more
specialized struct, but I don't think ident will ever go that way.

Hmm. I would be tempted to keep track of the file name and the line
number as well in IdentLine. One reason is that this can become
useful for debugging. A second is that this can reduce a bit the
arguments of fill_ident_line() and fill_hba_line() in hbafuncs.c once
we track these in HbaLine and IdentLine. And HEAD is slightly
overdoing it in its interface for the line number, actually, as we
pass the line number twice: from {Ident,Hba}Line and the respective
field from TokenizedAuthLine.
--
Michael

#57Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#56)
4 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Oct 25, 2022 at 08:59:57PM +0900, Michael Paquier wrote:

Hmm. I would be tempted to keep track of the file name and the line
number as well in IdentLine. One reason is that this can become
useful for debugging. A second is that this can reduce a bit the
arguments of fill_ident_line() and fill_hba_line() in hbafuncs.c once
we track these in HbaLine and IdentLine.

Ok, I guess something like the attached v14 is what you want.

And HEAD is slightly
overdoing it in its interface for the line number, actually, as we
pass the line number twice: from {Ident,Hba}Line and the respective
field from TokenizedAuthLine.

That wouldn't be overdoing anymore if we remove the line number / filename from
the fill_*_line prototypes right?

Attachments:

v14-0001-Refactor-knowledge-of-origin-file-in-hba.c.patchtext/plain; charset=us-asciiDownload
From ecc27b9101acf40f4888da8be033c70e6f21358a Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 25 Oct 2022 15:17:27 +0900
Subject: [PATCH v14 1/5] Refactor knowledge of origin file in hba.c

This limits the footprint of HbaFileName and IdentFileName to their
entry loading point, easing the introduction of the inclusion logic.

Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/libpq/hba.c | 114 +++++++++++++++++++++-------------------
 src/include/libpq/hba.h |   3 ++
 2 files changed, 63 insertions(+), 54 deletions(-)

diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index ea92f02a47..56bbe31dfd 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -641,6 +641,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 
 			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
 			tok_line->fields = current_line;
+			tok_line->file_name = pstrdup(filename);
 			tok_line->line_num = line_number;
 			tok_line->raw_line = pstrdup(buf.data);
 			tok_line->err_msg = err_msg;
@@ -984,7 +985,7 @@ do { \
 			 errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
 					optname, _(validmethods)), \
 			 errcontext("line %d of configuration file \"%s\"", \
-					line_num, HbaFileName))); \
+					line_num, file_name))); \
 	*err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \
 						optname, validmethods); \
 	return false; \
@@ -1004,7 +1005,7 @@ do { \
 				 errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
 						authname, argname), \
 				 errcontext("line %d of configuration file \"%s\"", \
-						line_num, HbaFileName))); \
+						line_num, file_name))); \
 		*err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \
 							authname, argname); \
 		return NULL; \
@@ -1027,7 +1028,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("missing entry at end of line"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("missing entry at end of line"); \
 		return NULL; \
 	} \
@@ -1040,7 +1041,7 @@ do { \
 				(errcode(ERRCODE_CONFIG_FILE_ERROR), \
 				 errmsg("multiple values in ident field"), \
 				 errcontext("line %d of configuration file \"%s\"", \
-							line_num, IdentFileName))); \
+							line_num, file_name))); \
 		*err_msg = pstrdup("multiple values in ident field"); \
 		return NULL; \
 	} \
@@ -1063,6 +1064,7 @@ HbaLine *
 parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	char	   *str;
 	struct addrinfo *gai_result;
@@ -1077,6 +1079,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 	HbaLine    *parsedline;
 
 	parsedline = palloc0(sizeof(HbaLine));
+	parsedline->sourcefile = pstrdup(file_name);
 	parsedline->linenumber = line_num;
 	parsedline->rawline = pstrdup(tok_line->raw_line);
 
@@ -1091,7 +1094,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for connection type"),
 				 errhint("Specify exactly one connection type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for connection type";
 		return NULL;
 	}
@@ -1119,7 +1122,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("hostssl record cannot match because SSL is disabled"),
 						 errhint("Set ssl = on in postgresql.conf."),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "hostssl record cannot match because SSL is disabled";
 			}
 #else
@@ -1127,7 +1130,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostssl record cannot match because SSL is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostssl record cannot match because SSL is not supported by this build";
 #endif
 		}
@@ -1139,7 +1142,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build";
 #endif
 		}
@@ -1160,7 +1163,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid connection type \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid connection type \"%s\"", token->string);
 		return NULL;
 	}
@@ -1173,7 +1176,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before database specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before database specification";
 		return NULL;
 	}
@@ -1184,7 +1187,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 		AuthToken  *tok = copy_auth_token(lfirst(tokencell));
 
 		/* Compile a regexp for the database token, if necessary */
-		if (regcomp_auth_token(tok, HbaFileName, line_num, err_msg, elevel))
+		if (regcomp_auth_token(tok, file_name, line_num, err_msg, elevel))
 			return NULL;
 
 		parsedline->databases = lappend(parsedline->databases, tok);
@@ -1198,7 +1201,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before role specification"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before role specification";
 		return NULL;
 	}
@@ -1209,7 +1212,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 		AuthToken  *tok = copy_auth_token(lfirst(tokencell));
 
 		/* Compile a regexp from the role token, if necessary */
-		if (regcomp_auth_token(tok, HbaFileName, line_num, err_msg, elevel))
+		if (regcomp_auth_token(tok, file_name, line_num, err_msg, elevel))
 			return NULL;
 
 		parsedline->roles = lappend(parsedline->roles, tok);
@@ -1225,7 +1228,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("end-of-line before IP address specification"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "end-of-line before IP address specification";
 			return NULL;
 		}
@@ -1237,7 +1240,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					 errmsg("multiple values specified for host address"),
 					 errhint("Specify one address range per line."),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "multiple values specified for host address";
 			return NULL;
 		}
@@ -1296,7 +1299,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						 errmsg("invalid IP address \"%s\": %s",
 								str, gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("invalid IP address \"%s\": %s",
 									str, gai_strerror(ret));
 				if (gai_result)
@@ -1316,7 +1319,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("specifying both host name and CIDR mask is invalid: \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"",
 										token->string);
 					return NULL;
@@ -1330,7 +1333,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid CIDR mask in address \"%s\"",
 									token->string),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid CIDR mask in address \"%s\"",
 										token->string);
 					return NULL;
@@ -1350,7 +1353,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("end-of-line before netmask specification"),
 							 errhint("Specify an address range in CIDR notation, or provide a separate netmask."),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "end-of-line before netmask specification";
 					return NULL;
 				}
@@ -1361,7 +1364,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("multiple values specified for netmask"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "multiple values specified for netmask";
 					return NULL;
 				}
@@ -1376,7 +1379,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							 errmsg("invalid IP mask \"%s\": %s",
 									token->string, gai_strerror(ret)),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = psprintf("invalid IP mask \"%s\": %s",
 										token->string, gai_strerror(ret));
 					if (gai_result)
@@ -1395,7 +1398,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							(errcode(ERRCODE_CONFIG_FILE_ERROR),
 							 errmsg("IP address and mask do not match"),
 							 errcontext("line %d of configuration file \"%s\"",
-										line_num, HbaFileName)));
+										line_num, file_name)));
 					*err_msg = "IP address and mask do not match";
 					return NULL;
 				}
@@ -1411,7 +1414,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("end-of-line before authentication method"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "end-of-line before authentication method";
 		return NULL;
 	}
@@ -1423,7 +1426,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("multiple values specified for authentication type"),
 				 errhint("Specify exactly one authentication type per line."),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "multiple values specified for authentication type";
 		return NULL;
 	}
@@ -1460,7 +1463,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled";
 			return NULL;
 		}
@@ -1501,7 +1504,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\"",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\"",
 							token->string);
 		return NULL;
@@ -1514,7 +1517,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				 errmsg("invalid authentication method \"%s\": not supported by this build",
 						token->string),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("invalid authentication method \"%s\": not supported by this build",
 							token->string);
 		return NULL;
@@ -1536,7 +1539,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("gssapi authentication is not supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "gssapi authentication is not supported on local sockets";
 		return NULL;
 	}
@@ -1548,7 +1551,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("peer authentication is only supported on local sockets"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "peer authentication is only supported on local sockets";
 		return NULL;
 	}
@@ -1566,7 +1569,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 				(errcode(ERRCODE_CONFIG_FILE_ERROR),
 				 errmsg("cert authentication is only supported on hostssl connections"),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = "cert authentication is only supported on hostssl connections";
 		return NULL;
 	}
@@ -1616,7 +1619,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("authentication option not in name=value format: %s", token->string),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = psprintf("authentication option not in name=value format: %s",
 									token->string);
 				return NULL;
@@ -1660,7 +1663,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix";
 				return NULL;
 			}
@@ -1671,7 +1674,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(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)));
+								line_num, file_name)));
 			*err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set";
 			return NULL;
 		}
@@ -1687,7 +1690,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter";
 			return NULL;
 		}
@@ -1704,7 +1707,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS servers cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS servers cannot be empty";
 			return NULL;
 		}
@@ -1715,7 +1718,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("list of RADIUS secrets cannot be empty"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "list of RADIUS secrets cannot be empty";
 			return NULL;
 		}
@@ -1734,7 +1737,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiussecrets),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiussecrets),
 								list_length(parsedline->radiusservers));
@@ -1750,7 +1753,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusports),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusports),
 								list_length(parsedline->radiusservers));
@@ -1766,7 +1769,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
 							list_length(parsedline->radiusidentifiers),
 							list_length(parsedline->radiusservers)),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)",
 								list_length(parsedline->radiusidentifiers),
 								list_length(parsedline->radiusservers));
@@ -1801,6 +1804,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				   int elevel, char **err_msg)
 {
 	int			line_num = hbaline->linenumber;
+	char	   *file_name = hbaline->sourcefile;
 
 #ifdef USE_LDAP
 	hbaline->ldapscope = LDAP_SCOPE_SUBTREE;
@@ -1824,7 +1828,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientcert can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientcert can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1841,7 +1845,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				*err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication";
 				return false;
 			}
@@ -1854,7 +1858,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientcert: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1866,7 +1870,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("clientname can only be configured for \"hostssl\" rows"),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = "clientname can only be configured for \"hostssl\" rows";
 			return false;
 		}
@@ -1885,7 +1889,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid value for clientname: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 	}
@@ -1971,7 +1975,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid ldapscheme value: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 		hbaline->ldapscheme = pstrdup(val);
 	}
 	else if (strcmp(name, "ldapserver") == 0)
@@ -1989,7 +1993,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					(errcode(ERRCODE_CONFIG_FILE_ERROR),
 					 errmsg("invalid LDAP port number: \"%s\"", val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid LDAP port number: \"%s\"", val);
 			return false;
 		}
@@ -2083,7 +2087,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS server list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2102,7 +2106,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						 errmsg("could not translate RADIUS server name \"%s\" to address: %s",
 								(char *) lfirst(l), gai_strerror(ret)),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 				if (gai_result)
 					pg_freeaddrinfo_all(hints.ai_family, gai_result);
 
@@ -2131,7 +2135,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS port list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			*err_msg = psprintf("invalid RADIUS port number: \"%s\"", val);
 			return false;
 		}
@@ -2144,7 +2148,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("invalid RADIUS port number: \"%s\"", val),
 						 errcontext("line %d of configuration file \"%s\"",
-									line_num, HbaFileName)));
+									line_num, file_name)));
 
 				return false;
 			}
@@ -2167,7 +2171,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS secret list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2189,7 +2193,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 					 errmsg("could not parse RADIUS identifiers list \"%s\"",
 							val),
 					 errcontext("line %d of configuration file \"%s\"",
-								line_num, HbaFileName)));
+								line_num, file_name)));
 			return false;
 		}
 
@@ -2203,7 +2207,7 @@ parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 				 errmsg("unrecognized authentication option name: \"%s\"",
 						name),
 				 errcontext("line %d of configuration file \"%s\"",
-							line_num, HbaFileName)));
+							line_num, file_name)));
 		*err_msg = psprintf("unrecognized authentication option name: \"%s\"",
 							name);
 		return false;
@@ -2460,6 +2464,7 @@ IdentLine *
 parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 {
 	int			line_num = tok_line->line_num;
+	char	   *file_name = tok_line->file_name;
 	char	  **err_msg = &tok_line->err_msg;
 	ListCell   *field;
 	List	   *tokens;
@@ -2471,6 +2476,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 
 	parsedline = palloc0(sizeof(IdentLine));
 	parsedline->linenumber = line_num;
+	parsedline->sourcefile = pstrdup(file_name);
 
 	/* Get the map token (must exist) */
 	tokens = lfirst(field);
@@ -2500,7 +2506,7 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
 	 * Now that the field validation is done, compile a regex from the user
 	 * token, if necessary.
 	 */
-	if (regcomp_auth_token(parsedline->token, IdentFileName, line_num,
+	if (regcomp_auth_token(parsedline->token, file_name, line_num,
 						   err_msg, elevel))
 	{
 		/* err_msg includes the error to report */
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index cec2e2665f..bf896ac084 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -93,6 +93,7 @@ typedef struct AuthToken
 
 typedef struct HbaLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 	char	   *rawline;
 	ConnType	conntype;
@@ -138,6 +139,7 @@ typedef struct HbaLine
 
 typedef struct IdentLine
 {
+	char	   *sourcefile;
 	int			linenumber;
 
 	char	   *usermap;
@@ -157,6 +159,7 @@ typedef struct IdentLine
 typedef struct TokenizedAuthLine
 {
 	List	   *fields;			/* List of lists of AuthTokens */
+	char	   *file_name;		/* File name of origin */
 	int			line_num;		/* Line number */
 	char	   *raw_line;		/* Raw line text */
 	char	   *err_msg;		/* Error message if any */
-- 
2.37.0

v14-0002-Simplify-fill_hba_line-and-fill_ident_line-proto.patchtext/plain; charset=us-asciiDownload
From 9208a0ab88a005c969badf1d37bcbaa831dd98cc Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Wed, 26 Oct 2022 10:52:15 +0800
Subject: [PATCH v14 2/5] Simplify fill_hba_line and fill_ident_line
 prototypes.

There's no need to pass the line number as an explicit argument as the
information is already present in the HbaLine/IdentLine.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/backend/utils/adt/hbafuncs.c | 20 ++++++++------------
 1 file changed, 8 insertions(+), 12 deletions(-)

diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index cfdc4d8b39..f2a4f105c7 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,10 +26,10 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int lineno, HbaLine *hba, const char *err_msg);
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int lineno, IdentLine *ident, const char *err_msg);
+							IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -165,7 +165,6 @@ get_hba_options(HbaLine *hba)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
- * lineno: pg_hba.conf line number (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -174,7 +173,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int lineno, HbaLine *hba, const char *err_msg)
+			 HbaLine *hba, const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -194,7 +193,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	index = 0;
 
 	/* line_number */
-	values[index++] = Int32GetDatum(lineno);
+	values[index++] = Int32GetDatum(hba->linenumber);
 
 	if (hba != NULL)
 	{
@@ -393,8 +392,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, tok_line->line_num,
-					  hbaline, tok_line->err_msg);
+		fill_hba_line(tuple_store, tupdesc, hbaline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -439,7 +437,6 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
- * lineno: pg_ident.conf line number (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -448,7 +445,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int lineno, IdentLine *ident, const char *err_msg)
+				IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -462,7 +459,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	index = 0;
 
 	/* line_number */
-	values[index++] = Int32GetDatum(lineno);
+	values[index++] = Int32GetDatum(ident->linenumber);
 
 	if (ident != NULL)
 	{
@@ -529,8 +526,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, tok_line->line_num, identline,
-						tok_line->err_msg);
+		fill_ident_line(tuple_store, tupdesc, identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
-- 
2.37.0

v14-0003-Add-rule_number-mapping_number-to-the-pg_hba-pg_.patchtext/plain; charset=us-asciiDownload
From 9768e93a34d191337dc7db4c384833b5484722d6 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Mon, 30 May 2022 10:59:51 +0800
Subject: [PATCH v14 3/5] Add rule_number / mapping_number to the
 pg_hba/pg_ident views.

Author: Julien Rouhaud
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/system-views.sgml      | 22 ++++++++++++++
 src/backend/utils/adt/hbafuncs.c    | 47 ++++++++++++++++++++++-------
 src/include/catalog/pg_proc.dat     | 11 ++++---
 src/test/regress/expected/rules.out | 10 +++---
 4 files changed, 70 insertions(+), 20 deletions(-)

diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 1ca7c3f9bf..4723f712a7 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -991,6 +991,18 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rule_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Rule number of this rule among all rules if the rule is valid, otherwise
+       null. This indicates the order in which each rule will be considered
+       until the first matching one, if any, is used to perform authentication
+       with the client.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
@@ -1131,6 +1143,16 @@
     </thead>
 
     <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>mapping_number</structfield> <type>int4</type>
+      </para>
+      <para>
+       Mapping number, in priority order, of this mapping if the mapping is
+       valid, otherwise null
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index f2a4f105c7..255109ab07 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,10 +26,11 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  HbaLine *hba, const char *err_msg);
+						  int rule_number, HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							IdentLine *ident, const char *err_msg);
+							int mapping_number, IdentLine *ident,
+							const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -157,7 +158,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 9
+#define NUM_PG_HBA_FILE_RULES_ATTS	 10
 
 /*
  * fill_hba_line
@@ -165,6 +166,7 @@ get_hba_options(HbaLine *hba)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * rule_number: unique rule identifier among all valid rules
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -173,7 +175,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			 HbaLine *hba, const char *err_msg)
+			  int rule_number, HbaLine *hba, const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
 	bool		nulls[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -192,6 +194,11 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* rule_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(rule_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(hba->linenumber);
 
@@ -335,7 +342,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -358,6 +365,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *hba_lines = NIL;
 	ListCell   *line;
+	int			rule_number = 0;
 	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
@@ -392,7 +400,12 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			hbaline = parse_hba_line(tok_line, DEBUG3);
 
-		fill_hba_line(tuple_store, tupdesc, hbaline, tok_line->err_msg);
+		/* No error, set a new rule number */
+		if (tok_line->err_msg == NULL)
+			rule_number++;
+
+		fill_hba_line(tuple_store, tupdesc, rule_number, hbaline,
+					  tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -428,8 +441,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 	PG_RETURN_NULL();
 }
 
-/* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 5
+/* Number of columns in pg_hba_file_mappings view */
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -437,6 +450,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  *
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
+ * mapping_number: unique rule identifier among all valid rules
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -445,7 +459,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				IdentLine *ident, const char *err_msg)
+				int mapping_number, IdentLine *ident, const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
 	bool		nulls[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -458,6 +472,11 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	memset(nulls, 0, sizeof(nulls));
 	index = 0;
 
+	/* mapping_number */
+	if (err_msg)
+		nulls[index++] = true;
+	else
+		values[index++] = Int32GetDatum(mapping_number);
 	/* line_number */
 	values[index++] = Int32GetDatum(ident->linenumber);
 
@@ -470,7 +489,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[1], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 2) * sizeof(bool));
+		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
 	}
 
 	/* error */
@@ -492,6 +511,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	FILE	   *file;
 	List	   *ident_lines = NIL;
 	ListCell   *line;
+	int			mapping_number = 0;
 	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
@@ -526,7 +546,12 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			identline = parse_ident_line(tok_line, DEBUG3);
 
-		fill_ident_line(tuple_store, tupdesc, identline, tok_line->err_msg);
+		/* No error, set a new mapping number */
+		if (tok_line->err_msg == NULL)
+			mapping_number++;
+
+		fill_ident_line(tuple_store, tupdesc, mapping_number, identline,
+						tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 62a5b8e655..4f5d05d0ce 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,15 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o}',
-  proargnames => '{line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,text,text,text,text}', proargmodes => '{o,o,o,o,o}',
-  proargnames => '{line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o}',
+  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index bfcd8ac9a0..178e536e21 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1337,7 +1337,8 @@ pg_group| SELECT pg_authid.rolname AS groname,
           WHERE (pg_auth_members.roleid = pg_authid.oid)) AS grolist
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
-pg_hba_file_rules| SELECT a.line_number,
+pg_hba_file_rules| SELECT a.rule_number,
+    a.line_number,
     a.type,
     a.database,
     a.user_name,
@@ -1346,13 +1347,14 @@ pg_hba_file_rules| SELECT a.line_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(line_number, type, database, user_name, address, netmask, auth_method, options, error);
-pg_ident_file_mappings| SELECT a.line_number,
+   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+pg_ident_file_mappings| SELECT a.mapping_number,
+    a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

v14-0004-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/plain; charset=us-asciiDownload
From a37db4c56468bc7f41e14e75acf6f4c266eafd4c Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Tue, 25 Oct 2022 15:26:50 +0900
Subject: [PATCH v14 4/5] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  22 +-
 src/backend/libpq/hba.c                       | 374 ++++++++--
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  20 +-
 src/backend/utils/misc/guc-file.l             | 229 +++---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/include/libpq/hba.h                       |   3 +-
 src/include/utils/guc.h                       |   2 +
 .../authentication/t/003_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 12 files changed, 1239 insertions(+), 212 deletions(-)
 create mode 100644 src/test/authentication/t/003_file_inclusion.pl

diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..2ae723de66 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -863,8 +914,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -875,6 +928,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 4723f712a7..7d1cec8b7f 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1003,12 +1003,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1153,12 +1162,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this mapping
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_ident.conf</filename>
+       Line number of this mapping in the given <literal>file_name</literal>
       </para></entry>
      </row>
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 56bbe31dfd..74f46ffb04 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -22,6 +22,7 @@
 #include <sys/param.h>
 #include <sys/socket.h>
 #include <netdb.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -70,6 +71,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -115,14 +122,26 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int depth, int elevel,
+							   char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 							   char **err_msg, int elevel);
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   bool strict, const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -413,7 +432,7 @@ regexec_auth_token(const char *match, AuthToken *token, size_t nmatch,
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int depth, int elevel, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -429,7 +448,7 @@ next_field_expand(const char *filename, char **lineptr,
 
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
-			tokens = tokenize_inc_file(tokens, filename, buf + 1,
+			tokens = tokenize_inc_file(tokens, filename, buf + 1, depth + 1,
 									   elevel, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
@@ -457,6 +476,7 @@ static List *
 tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
+				  int depth,
 				  int elevel,
 				  char **err_msg)
 {
@@ -466,39 +486,30 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
 	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
+		*err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", *err_msg)));
+		return tokens;
 	}
 
-	inc_file = AllocateFile(inc_fullname, "r");
-	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, true,
+							 outer_filename, elevel, err_msg, &inc_fullname);
 
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
+	if (inc_file == NULL)
 		return tokens;
-	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, depth + 1,
+								 elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -536,11 +547,38 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -549,30 +587,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int depth, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -625,7 +655,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		{
 			List	   *current_field;
 
-			current_field = next_field_expand(filename, &lineptr,
+			current_field = next_field_expand(filename, &lineptr, depth,
 											  elevel, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
@@ -633,30 +663,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
+
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
 		{
-			TokenizedAuthLine *tok_line;
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetDirConfFiles(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->file_name = pstrdup(filename);
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -2355,7 +2482,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2445,6 +2572,135 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename, bool strict,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		if (strict)
+		{
+			switch (kind)
+			{
+				case SecondaryAuthFile:
+					msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+					msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+					break;
+				case IncludedAuthFile:
+					msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+					msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+					break;
+				default:
+					elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+					break;
+			}
+
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg(msglog, inc_filename, *inc_fullname)));
+			*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+								strerror(save_errno));
+		}
+		else
+		{
+			Assert(kind == IncludedAuthFile);
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							*inc_fullname)));
+		}
+
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", err_msg)));
+		return err_msg;
+	}
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, strict,
+							 outer_filename, elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_inc_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+			return NULL;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, depth, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2728,7 +2984,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 255109ab07..d356804c5a 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -158,7 +158,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -199,6 +199,8 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(rule_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(hba->sourcefile);
 	/* line_number */
 	values[index++] = Int32GetDatum(hba->linenumber);
 
@@ -342,7 +344,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -383,7 +385,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open configuration file \"%s\": %m",
 						HbaFileName)));
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -442,7 +444,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_hba_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -477,6 +479,8 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 		nulls[index++] = true;
 	else
 		values[index++] = Int32GetDatum(mapping_number);
+	/* file_name */
+	values[index++] = CStringGetTextDatum(ident->sourcefile);
 	/* line_number */
 	values[index++] = Int32GetDatum(ident->linenumber);
 
@@ -489,7 +493,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -529,7 +533,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open usermap file \"%s\": %m",
 						IdentFileName)));
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -550,8 +554,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 		if (tok_line->err_msg == NULL)
 			mapping_number++;
 
-		fill_ident_line(tuple_store, tupdesc, mapping_number, identline,
-						tok_line->err_msg);
+		fill_ident_line(tuple_store, tupdesc, mapping_number,
+						identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index 721628c0cf..86b6cc1c8a 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -345,6 +345,110 @@ GUC_flex_fatal(const char *msg)
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * Returns the list of config files located in a directory, in alphabetical
+ * order.
+ *
+ * We don't check for recursion or too-deep nesting depth here, its up to the
+ * caller to take care of that.
+ */
+char **
+GetDirConfFiles(const char *includedir, const char *calling_file, int elevel,
+				int *num_filenames, char **err_msg)
+{
+	char	   *directory;
+	DIR		   *d;
+	struct dirent *de;
+	char	  **filenames;
+	int			size_filenames;
+
+	/*
+	 * Reject directory name that is all-blank (including empty), as that
+	 * leads to confusion --- we'd read the containing directory, typically
+	 * resulting in recursive inclusion of the same file(s).
+	 */
+	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty configuration directory name: \"%s\"",
+						includedir)));
+		*err_msg = "empty configuration directory name";
+		return NULL;
+	}
+
+	directory = AbsoluteConfigLocation(includedir, calling_file);
+	d = AllocateDir(directory);
+	if (d == NULL)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration directory \"%s\": %m",
+						directory)));
+		*err_msg = psprintf("could not open directory \"%s\"", directory);
+		filenames = NULL;
+		goto cleanup;
+	}
+
+	/*
+	 * Read the directory and put the filenames in an array, so we can sort
+	 * them prior to caller processing the contents.
+	 */
+	size_filenames = 32;
+	filenames = (char **) palloc(size_filenames * sizeof(char *));
+	*num_filenames = 0;
+
+	while ((de = ReadDir(d, directory)) != NULL)
+	{
+		PGFileType	de_type;
+		char		filename[MAXPGPATH];
+
+		/*
+		 * Only parse files with names ending in ".conf".  Explicitly reject
+		 * files starting with ".".  This excludes things like "." and "..",
+		 * as well as typical hidden files, backup files, and editor debris.
+		 */
+		if (strlen(de->d_name) < 6)
+			continue;
+		if (de->d_name[0] == '.')
+			continue;
+		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+			continue;
+
+		join_path_components(filename, directory, de->d_name);
+		canonicalize_path(filename);
+		de_type = get_dirent_type(filename, de, true, elevel);
+		if (de_type == PGFILETYPE_ERROR)
+		{
+			*err_msg = psprintf("could not stat file \"%s\"", filename);
+			pfree(filenames);
+			filenames = NULL;
+			goto cleanup;
+		}
+		else if (de_type != PGFILETYPE_DIR)
+		{
+			/* Add file to array, increasing its size in blocks of 32 */
+			if (*num_filenames >= size_filenames)
+			{
+				size_filenames += 32;
+				filenames = (char **) repalloc(filenames,
+										size_filenames * sizeof(char *));
+			}
+			filenames[*num_filenames] = pstrdup(filename);
+			(*num_filenames)++;
+		}
+	}
+
+	if (*num_filenames > 0)
+		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
+
+cleanup:
+	if (d)
+		FreeDir(d);
+	pfree(directory);
+	return filenames;
+}
+
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -606,127 +710,30 @@ ParseConfigDirectory(const char *includedir,
 					 ConfigVariable **head_p,
 					 ConfigVariable **tail_p)
 {
-	char	   *directory;
-	DIR		   *d;
-	struct dirent *de;
+	char	   *err_msg;
 	char	  **filenames;
 	int			num_filenames;
-	int			size_filenames;
-	bool		status;
-
-	/*
-	 * Reject directory name that is all-blank (including empty), as that
-	 * leads to confusion --- we'd read the containing directory, typically
-	 * resulting in recursive inclusion of the same file(s).
-	 */
-	if (strspn(includedir, " \t\r\n") == strlen(includedir))
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("empty configuration directory name: \"%s\"",
-						includedir)));
-		record_config_file_error("empty configuration directory name",
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		return false;
-	}
-
-	/*
-	 * We don't check for recursion or too-deep nesting depth here; the
-	 * subsequent calls to ParseConfigFile will take care of that.
-	 */
-
-	directory = AbsoluteConfigLocation(includedir, calling_file);
-	d = AllocateDir(directory);
-	if (d == NULL)
-	{
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration directory \"%s\": %m",
-						directory)));
-		record_config_file_error(psprintf("could not open directory \"%s\"",
-										  directory),
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		status = false;
-		goto cleanup;
-	}
 
-	/*
-	 * Read the directory and put the filenames in an array, so we can sort
-	 * them prior to processing the contents.
-	 */
-	size_filenames = 32;
-	filenames = (char **) palloc(size_filenames * sizeof(char *));
-	num_filenames = 0;
+	filenames = GetDirConfFiles(includedir, calling_file, elevel,
+							   &num_filenames, &err_msg);
 
-	while ((de = ReadDir(d, directory)) != NULL)
+	if (!filenames)
 	{
-		PGFileType	de_type;
-		char		filename[MAXPGPATH];
-
-		/*
-		 * Only parse files with names ending in ".conf".  Explicitly reject
-		 * files starting with ".".  This excludes things like "." and "..",
-		 * as well as typical hidden files, backup files, and editor debris.
-		 */
-		if (strlen(de->d_name) < 6)
-			continue;
-		if (de->d_name[0] == '.')
-			continue;
-		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
-			continue;
-
-		join_path_components(filename, directory, de->d_name);
-		canonicalize_path(filename);
-		de_type = get_dirent_type(filename, de, true, elevel);
-		if (de_type == PGFILETYPE_ERROR)
-		{
-			record_config_file_error(psprintf("could not stat file \"%s\"",
-											  filename),
-									 calling_file, calling_lineno,
-									 head_p, tail_p);
-			status = false;
-			goto cleanup;
-		}
-		else if (de_type != PGFILETYPE_DIR)
-		{
-			/* Add file to array, increasing its size in blocks of 32 */
-			if (num_filenames >= size_filenames)
-			{
-				size_filenames += 32;
-				filenames = (char **) repalloc(filenames,
-											   size_filenames * sizeof(char *));
-			}
-			filenames[num_filenames] = pstrdup(filename);
-			num_filenames++;
-		}
+		record_config_file_error(err_msg, calling_file, calling_lineno, head_p,
+								 tail_p);
+		return false;
 	}
 
-	if (num_filenames > 0)
+	for (int i = 0; i < num_filenames; i++)
 	{
-		int			i;
-
-		qsort(filenames, num_filenames, sizeof(char *), pg_qsort_strcmp);
-		for (i = 0; i < num_filenames; i++)
-		{
-			if (!ParseConfigFile(filenames[i], true,
-								 calling_file, calling_lineno,
-								 depth, elevel,
-								 head_p, tail_p))
-			{
-				status = false;
-				goto cleanup;
-			}
-		}
+		if (!ParseConfigFile(filenames[i], true,
+							 calling_file, calling_lineno,
+							 depth, elevel,
+							 head_p, tail_p))
+			return false;
 	}
-	status = true;
 
-cleanup:
-	if (d)
-		FreeDir(d);
-	pfree(directory);
-	return status;
+	return true;
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 4f5d05d0ce..2ad06c4d3e 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,16 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{mapping_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{mapping_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index bf896ac084..7108cd2dae 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -179,6 +179,7 @@ extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int depth,
+										int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index b3aaff9665..59ca39d908 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -144,6 +144,8 @@ typedef struct ConfigVariable
 	struct ConfigVariable *next;
 } ConfigVariable;
 
+extern char **GetDirConfFiles(const char *includedir, const char *calling_file,
+							  int elevel, int *num_filenames, char **err_msg);
 extern bool ParseConfigFile(const char *config_file, bool strict,
 							const char *calling_file, int calling_lineno,
 							int depth, int elevel,
diff --git a/src/test/authentication/t/003_file_inclusion.pl b/src/test/authentication/t/003_file_inclusion.pl
new file mode 100644
index 0000000000..8eae72b8d4
--- /dev/null
+++ b/src/test/authentication/t/003_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open secondary authentication file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open included authentication file "\.\.\/not_a_file" as "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open secondary authentication file \"\@dbnames_fails.conf\" as \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	'could not open configuration file "recurse.conf": maximum nesting depth exceeded');
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open secondary authentication file \"\@osnames_fails.conf\" as \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	'could not open configuration file "ident_recurse.conf": maximum nesting depth exceeded');
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT mapping_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY mapping_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 178e536e21..3d8f182674 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.mapping_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(mapping_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(mapping_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
-- 
2.37.0

#58Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#57)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Oct 26, 2022 at 11:19:48AM +0800, Julien Rouhaud wrote:

That wouldn't be overdoing anymore if we remove the line number / filename from
the fill_*_line prototypes right?

Yeah, but there is a twist: HbaLine or IdentLine can be passed as
NULL when entering in fill_hba_line() or fill_ident_line() in the
event of an error when tokenizing the line or when failing the
creation of their Line, so attempting to read the line number from
either of them when filling in a tuple for their respective view would
cause a crash.

So, for now, I have taken the minimalistic approach with the addition
of the source file name in HbaFile and in TokenizedAuthLine. The
former is actually necessary anyway as auth.c wants it in two
locations (improved auth_failed() and set_authn_id()).

It is still true that the line number in IdentLine remains unused.
Hence, do you think that the addition of the source file name and its
line number could be useful for the error reporting done in
check_ident_usermap()? The contents of the HbaLines are used in
auth.c for its own reporting magic, and it looks like this would be
the only location where what's in the IdentLines is useful, aka
provide more details about what is happening. Once users are able to
include ident files, that would likely help in debugging issues.
--
Michael

#59Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#57)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Oct 26, 2022 at 11:19:48AM +0800, Julien Rouhaud wrote:

That wouldn't be overdoing anymore if we remove the line number / filename from
the fill_*_line prototypes right?

So, I have spent a good portion of today looking at what you have
here, applying 0001 and 0003 while fixing, rebasing and testing the
whole, discarding 0002 (we could do more for the line number and
source file in terms of the LOGs reported for a regexec failure).

Now remains 0004, which is the core of the proposal, and while it
needs a rebase, I have not been able to spend much time looking at its
internals. In order to help with the follow-up steps, I have spotted
a few areas that could be done first:
- The diffs in guc.h/c for the introduction of GetDirConfFiles() to
get a set of files in a directory (got to think a bit more about
ParseConfigFp() when it comes to the keywords, but that comes to play
with the parsing logic which is different).
- The TAP test, which is half the size of the patch in line number.
Could it be possible to make it more edible, introducing a basic
infrastructure to check a set of rules in pg_hba.conf and
pg_ident.conf without the inclusion logic? Checks for error patterns
(that I agree we strongly lack tests for) look like something we'd
want to tackle independently of the inclusion logic, and it should be
built on top of a basic test infra, at least.
--
Michael

#60Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#59)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Oct 26, 2022 at 03:56:07PM +0900, Michael Paquier wrote:

So, I have spent a good portion of today looking at what you have
here, applying 0001 and 0003 while fixing, rebasing and testing the
whole, discarding 0002 (we could do more for the line number and
source file in terms of the LOGs reported for a regexec failure).

Thanks!

Now remains 0004, which is the core of the proposal, and while it
needs a rebase,

Have you already done a rebase while working on the patch or are you intending
to take care of it, or should I? Let's no both do the work :)

- The TAP test, which is half the size of the patch in line number.
Could it be possible to make it more edible, introducing a basic
infrastructure to check a set of rules in pg_hba.conf and
pg_ident.conf without the inclusion logic? Checks for error patterns
(that I agree we strongly lack tests for) look like something we'd
want to tackle independently of the inclusion logic, and it should be
built on top of a basic test infra, at least.

I don't mind taking care of that, but before doing so I'd like to have some
feedback on whether you're ok with my approach (per my initial email about it
at [1]/messages/by-id/20220730080936.atyxodvwlmf2wnoc@jrouhaud) or if you had some different
ideas on how to do it.

[1]: /messages/by-id/20220730080936.atyxodvwlmf2wnoc@jrouhaud

#61Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#60)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Oct 26, 2022 at 11:32:14PM +0800, Julien Rouhaud wrote:

I don't mind taking care of that, but before doing so I'd like to have some
feedback on whether you're ok with my approach (per my initial email about it
at [1]) or if you had some different
ideas on how to do it.

Putting things afresh, there are two different things here (sorry I
need to see that typed ;p):
1) How do we want to check reliably the loading of the HBA and ident
files on errors? EXEC_BACKEND would reload an entire new thing for
each connection, hence we need some loops to go through that.
2) How to check the contents of pg_hba_file_rules and
pg_ident_file_mappings?

There is a dependency between 1) and 2) once we try to check for error
patterns in pg_hba_file_rules, because connections would just not
happen. This is not the case for pg_ident_file_mappings though, so we
could still test for buggy patterns in pg_ident.conf (or any of its
included parts) with some expected content of
pg_ident_file_mappings.error after a successful connection.

Hmm. And what if we just gave up on the checks for error patterns in
pg_hba_file_rules? One thing that we could do for this part is to
include all the buggy patterns we want to check at once in pg_hba.conf
in its included portions, then scan for all the logs produced after
attempting to start a server as the loading of pg_hba.conf would
produce one LOG line with its CONTEXT for each buggy entry. The patch
checks for error patterns with generate_log_err_rows(), but it looks
like it would make the part 3 of the new test cleaner and easier to
maintain in the long-term.
--
Michael

#62Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#60)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Oct 26, 2022 at 11:32:14PM +0800, Julien Rouhaud wrote:

Have you already done a rebase while working on the patch or are you intending
to take care of it, or should I? Let's no both do the work :)

Spoiler alert: I have not done a rebase yet ;)
--
Michael

#63Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#61)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Thu, Oct 27, 2022 at 12:08:31PM +0900, Michael Paquier wrote:

Putting things afresh, there are two different things here (sorry I
need to see that typed ;p):
1) How do we want to check reliably the loading of the HBA and ident
files on errors?

I guess you meant the failure to load HBA / ident files containing invalid
data?

EXEC_BACKEND would reload an entire new thing for
each connection, hence we need some loops to go through that.
2) How to check the contents of pg_hba_file_rules and
pg_ident_file_mappings?

There is a dependency between 1) and 2) once we try to check for error
patterns in pg_hba_file_rules, because connections would just not
happen. This is not the case for pg_ident_file_mappings though, so we
could still test for buggy patterns in pg_ident.conf (or any of its
included parts) with some expected content of
pg_ident_file_mappings.error after a successful connection.

Hmm. And what if we just gave up on the checks for error patterns in
pg_hba_file_rules?

We discussed this problem in the past (1), and my understanding was that
detecting a -DEXEC_BACKEND/Win32 build and skipping those tests in that case
would be an acceptable solution to make sure there's at least some coverage.
The proposed patch adds such an approach, making sure that the failure is due
to an invalid HBA file. If you changed you mind I can remove that part, but
again I'd like to be sure of what you exactly want before starting to rewrite
stuff.

One thing that we could do for this part is to
include all the buggy patterns we want to check at once in pg_hba.conf
in its included portions, then scan for all the logs produced after
attempting to start a server as the loading of pg_hba.conf would
produce one LOG line with its CONTEXT for each buggy entry. The patch
checks for error patterns with generate_log_err_rows(), but it looks
like it would make the part 3 of the new test cleaner and easier to
maintain in the long-term.

I'm not sure what you mean here. The patch does check for all the errors
looking at LOG lines and CONTEXT lines, but to make the regexp easier it
doesn't try to make sure that each CONTEXT line is immediately following the
expected LOG line.

That's why the errors are divided in 2 steps: a first step with a single error
using some inclusion, so we can validate that the CONTEXT line is entirely
correct (wrt. line number and such), and then every possible error pattern
where we assume that the CONTEXT line are still following their LOG entry if
they're found. It also has the knowledge of which errors adds a CONTEXT line
and which don't. And that's done twice, for HBA and ident.

The part 3 is just concatenating everything back, for HBA and ident. So
long-term maintenance shouldn't get any harder as there won't be any need for
more steps. We can just keep appending stuff in the 2nd step and all the tests
should run as expected.

[1]: /messages/by-id/YtZLeJPlMutL9heh@paquier.xyz

#64Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#63)
Re: Allow file inclusion in pg_hba and pg_ident files

On Thu, Oct 27, 2022 at 12:26:25PM +0800, Julien Rouhaud wrote:

On Thu, Oct 27, 2022 at 12:08:31PM +0900, Michael Paquier wrote:

Putting things afresh, there are two different things here (sorry I
need to see that typed ;p):
1) How do we want to check reliably the loading of the HBA and ident
files on errors?

I guess you meant the failure to load HBA / ident files containing invalid
data?

Yeah.

Hmm. And what if we just gave up on the checks for error patterns in
pg_hba_file_rules?

One part that I was thinking about when typing this part yesterday is
that an EXEC_BACKEND build should work in non-WIN32 in TAP even if
pg_ident.conf cannot be loaded, but I forgot entirely about the part
where we need a user mapping for the SSPI authentication on WIN32, as
set by pg_regress.

We discussed this problem in the past (1), and my understanding was that
detecting a -DEXEC_BACKEND/Win32 build and skipping those tests in that case
would be an acceptable solution to make sure there's at least some coverage.
The proposed patch adds such an approach, making sure that the failure is due
to an invalid HBA file. If you changed you mind I can remove that part, but
again I'd like to be sure of what you exactly want before starting to rewrite
stuff.

I am still not completely sure what's the best way to do things here,
so let's do the following: let's keep the patch the way you think is
better for now (I may change my opinion on that but I'll hack that by
myself anyway). Using what you have as a base, could you split the
test and have it in its simplest to ease irs review? It would be able
to stress the buildfarm with a first version of the test and see how
it goes from there, and it is useful by itself IMO as HEAD has zero
coverage for this area.

I'm not sure what you mean here. The patch does check for all the errors
looking at LOG lines and CONTEXT lines, but to make the regexp easier it
doesn't try to make sure that each CONTEXT line is immediately following the
expected LOG line.

Hmm. Perhaps we'd better make sure that the LOG/CONTEXT link is
checked? The context includes the line number while a generic
sentence, and the LOG provides all the details of the error
happening.

That's why the errors are divided in 2 steps: a first step with a single error
using some inclusion, so we can validate that the CONTEXT line is entirely
correct (wrt. line number and such), and then every possible error pattern
where we assume that the CONTEXT line are still following their LOG entry if
they're found. It also has the knowledge of which errors adds a CONTEXT line
and which don't. And that's done twice, for HBA and ident.

Okay, so you do check the relationship between both, after all.

The part 3 is just concatenating everything back, for HBA and ident. So
long-term maintenance shouldn't get any harder as there won't be any need for
more steps. We can just keep appending stuff in the 2nd step and all the tests
should run as expected.

Hmm. Okay.
--
Michael

#65Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#64)
Re: Allow file inclusion in pg_hba and pg_ident files

On Fri, Oct 28, 2022 at 10:24:23AM +0900, Michael Paquier wrote:

On Thu, Oct 27, 2022 at 12:26:25PM +0800, Julien Rouhaud wrote:

I am still not completely sure what's the best way to do things here,
so let's do the following: let's keep the patch the way you think is
better for now (I may change my opinion on that but I'll hack that by
myself anyway). Using what you have as a base, could you split the
test and have it in its simplest to ease irs review? It would be able
to stress the buildfarm with a first version of the test and see how
it goes from there, and it is useful by itself IMO as HEAD has zero
coverage for this area.

To be honest I'd rather not to. It's excessively annoying to work on those
tests (I spent multiple days trying to make it as clean and readable as
possible), and splitting it to only test the current infrastructure will need
some substantial efforts.

But more importantly, the next commit that will add tests for file inclusion
will then be totally unmaintainable and unreadable, so that's IMO even worse.
I think it will probably either be the current file overwritten or a new one
written from scratch if some changes are done in the simplified test, and I'm
not volunteering to do that.

#66Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#65)
2 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Fri, Oct 28, 2022 at 11:49:54AM +0800, Julien Rouhaud wrote:

To be honest I'd rather not to. It's excessively annoying to work on those
tests (I spent multiple days trying to make it as clean and readable as
possible), and splitting it to only test the current infrastructure will need
some substantial efforts.

Well, I'd really like to split things as much as possible, beginning
with some solid basics before extending its capabilities. This
reduces the odds of introducing issues in-between, particularly in
areas as sensible as authentication that involves not-yet-logged-in
users.

But more importantly, the next commit that will add tests for file inclusion
will then be totally unmaintainable and unreadable, so that's IMO even worse.
I think it will probably either be the current file overwritten or a new one
written from scratch if some changes are done in the simplified test, and I'm
not volunteering to do that.

Not sure about that either.

Anyway, the last patch posted on this thread does not apply and the CF
bot fails, so it needed a rebase. I have first noticed that your
patch included some doc fixes independent of this thread, so I have
applied that as e765028 and backpatched it down to 15. The TAP test
needs to be renamed to 0004, and it was missing from meson.build,
hence the CI was not testing it.

I have spent some time starting at the logic today of the whole, and
GetConfFilesInDir() is really the first thing that stood out. I am
not sure that it makes much sense to keep that in guc-file.c, TBH,
once we feed it into hba.c. Perhaps we could put the refactored
routine (and AbsoluteConfigLocation as a side effect) into a new file
in misc/?

As of HEAD, tokenize_inc_file() is the place where we handle a list of
tokens included with an '@' file, appending the existing set of
AuthTokens into the list we are building by grabbing a copy of these
before deleting the line memory context.

Your patch proposes a different alternative, which is to pass down the
memory context created in tokenize_auth_file() down to the callers
with tokenize_file_with_context() dealing with all the internals.
process_included_auth_file() is different extension of that.
This may come down to a matter of taste, but could be be cleaner to
take an approach similar to tokenize_inc_file() and just create a copy
of the AuthToken list coming from a full file and append it to the
result rather than passing around the memory context for the lines?
This would lead to some simplifications, it seems, at least with the
number of arguments passed down across the layers.

The addition of a check for the depth in two places seems unnecessary,
and it looks like we should do this kind of check in only one place.
I have not tried yet, but if we actually move the AllocateFile() and
FreeFile() calls within tokenize_auth_file(), it looks like we may be
able to get to a simpler logic without the need of the with_context()
flavor (even no process_included_auth_file required)? That could make
the interface easier to follow as a whole, while limiting the presence
of AllocateFile() and FreeFile() to a single code path, impacting
open_inc_file() that relies on what the patch uses for
SecondaryAuthFile and IncludedAuthFile (we could attempt to use the
same error message everywhere as well, as one could expect that
expanded and included files have different names which is enough to
guess which one is an inclusion and which one is a secondary).

Attached are two patches: the first one is a rebase of what you have
posted, and the second one are some changes I did while playing with
the logic. In the second one, except for conffiles.{h.c}, the changes
are just a POC but that's to show the areas that I am planning to
rework, and your tests pass with it. I still need to think about all
that and reconsider the design of the interface that would fit with
the tokenization of the inclusions, without that many subroutines to
do the work as it makes the code harder to follow. Well, that's to
say that I am not staying idle :)
--
Michael

Attachments:

v15-0002-Some-simplifications-from-me.patchtext/x-diff; charset=us-asciiDownload
From 402d42283b872b1abd1038a9e640a67f8a3248b9 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 2 Nov 2022 15:45:41 +0900
Subject: [PATCH v15 2/2] Some simplifications from me

---
 src/include/utils/conffiles.h       |  23 ++++
 src/include/utils/guc.h             |   2 -
 src/backend/libpq/hba.c             |  66 ++++++------
 src/backend/utils/misc/Makefile     |   1 +
 src/backend/utils/misc/conffiles.c  | 161 ++++++++++++++++++++++++++++
 src/backend/utils/misc/guc-file.l   | 138 +-----------------------
 src/backend/utils/misc/meson.build  |   1 +
 src/test/authentication/meson.build |   1 +
 8 files changed, 221 insertions(+), 172 deletions(-)
 create mode 100644 src/include/utils/conffiles.h
 create mode 100644 src/backend/utils/misc/conffiles.c

diff --git a/src/include/utils/conffiles.h b/src/include/utils/conffiles.h
new file mode 100644
index 0000000000..3f23a2a011
--- /dev/null
+++ b/src/include/utils/conffiles.h
@@ -0,0 +1,23 @@
+/*--------------------------------------------------------------------
+ * conffiles.h
+ *
+ * Utilities related to configuration files.
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/utils/conffiles.h
+ *
+ *--------------------------------------------------------------------
+ */
+#ifndef CONFFILES_H
+#define CONFFILES_H
+
+extern char *AbsoluteConfigLocation(const char *location,
+									const char *calling_file);
+extern char **GetConfFilesInDir(const char *includedir,
+								const char *calling_file,
+								int elevel, int *num_filenames,
+								char **err_msg);
+
+#endif							/* CONFFILES_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index 59ca39d908..b3aaff9665 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -144,8 +144,6 @@ typedef struct ConfigVariable
 	struct ConfigVariable *next;
 } ConfigVariable;
 
-extern char **GetDirConfFiles(const char *includedir, const char *calling_file,
-							  int elevel, int *num_filenames, char **err_msg);
 extern bool ParseConfigFile(const char *config_file, bool strict,
 							const char *calling_file, int calling_lineno,
 							int depth, int elevel,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 1034ff8220..7ebbd5d7b1 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -22,7 +22,6 @@
 #include <sys/param.h>
 #include <sys/socket.h>
 #include <netdb.h>
-#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -42,6 +41,7 @@
 #include "storage/fd.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/conffiles.h"
 #include "utils/guc.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -122,10 +122,9 @@ static const char *const UserAuthName[] =
 };
 
 
-static void tokenize_file_with_context(MemoryContext linecxt,
-									   const char *filename, FILE *file,
-									   List **tok_lines, int depth,
-									   int elevel);
+static void tokenize_auth_file_internal(const char *filename, FILE *file,
+								   List **tok_lines, int depth,
+								   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int depth, int elevel,
 							   char **err_msg);
@@ -138,9 +137,9 @@ static int	regexec_auth_token(const char *match, AuthToken *token,
 static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
 						   bool strict, const char *outer_filename, int elevel,
 						   char **err_msg, char **inc_fullname);
-static char *process_included_authfile(const char *inc_filename, bool strict,
+static char *process_included_auth_file(const char *inc_filename, bool strict,
 									   const char *outer_filename, int depth,
-									   int elevel, MemoryContext linecxt,
+									   int elevel,
 									   List **tok_lines);
 
 
@@ -508,8 +507,8 @@ tokenize_inc_file(List *tokens,
 		return tokens;
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, depth + 1,
-								 elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines,
+								depth + 1, elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -548,8 +547,9 @@ tokenize_inc_file(List *tokens,
 /*
  * tokenize_auth_file
  *
- * Wrapper around tokenize_file_with_context, creating a dedicated memory
- * context.
+ * Wrapper around tokenize_auth_file_internal, creating a dedicated memory
+ * context where all the magic happens, working as an entry point for
+ * the tokenization of the HBA and ident files.
  *
  * Return value is this memory context which contains all memory allocated by
  * this function (it's a child of caller's context).
@@ -559,14 +559,19 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 				   int depth, int elevel)
 {
 	MemoryContext linecxt;
+	MemoryContext oldcxt;
+
 	linecxt = AllocSetContextCreate(CurrentMemoryContext,
 									"tokenize_auth_file",
 									ALLOCSET_SMALL_SIZES);
+	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	*tok_lines = NIL;
 
-	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
-							   elevel);
+	/* can recurse into itself */
+	tokenize_auth_file_internal(filename, file, tok_lines, depth, elevel);
+
+	MemoryContextSwitchTo(oldcxt);
 
 	return linecxt;
 }
@@ -577,8 +582,6 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
- * linecxt: memory context which must contain all memory allocated by the
- * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -589,14 +592,11 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
  * output list.
  */
 static void
-tokenize_file_with_context(MemoryContext linecxt, const char *filename,
-						   FILE *file, List **tok_lines, int depth, int elevel)
+tokenize_auth_file_internal(const char *filename,
+					   FILE *file, List **tok_lines, int depth, int elevel)
 {
 	StringInfoData buf;
 	int			line_number = 1;
-	MemoryContext oldcxt;
-
-	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
@@ -683,8 +683,8 @@ tokenize_file_with_context(MemoryContext linecxt, const char *filename,
 
 				inc_filename = second->string;
 
-				err_msg = process_included_authfile(inc_filename, true,
-										  filename, depth + 1, elevel, linecxt,
+				err_msg = process_included_auth_file(inc_filename, true,
+										  filename, depth + 1, elevel,
 										  tok_lines);
 
 				if (!err_msg)
@@ -703,7 +703,7 @@ tokenize_file_with_context(MemoryContext linecxt, const char *filename,
 				int			num_filenames;
 				StringInfoData err_buf;
 
-				filenames = GetDirConfFiles(dir_name, filename, elevel,
+				filenames = GetConfFilesInDir(dir_name, filename, elevel,
 						&num_filenames, &err_msg);
 
 				if (!filenames)
@@ -720,9 +720,9 @@ tokenize_file_with_context(MemoryContext linecxt, const char *filename,
 					 * overwritten at the end of the loop with the
 					 * cumulated errors, if any.
 					 */
-					err_msg = process_included_authfile(filenames[i], true,
+					err_msg = process_included_auth_file(filenames[i], true,
 												filename, depth + 1, elevel,
-												linecxt, tok_lines);
+												tok_lines);
 
 					/* Cumulate errors if any. */
 					if (err_msg)
@@ -749,8 +749,8 @@ tokenize_file_with_context(MemoryContext linecxt, const char *filename,
 
 				inc_filename = second->string;
 
-				err_msg = process_included_authfile(inc_filename, false,
-										  filename, depth + 1, elevel, linecxt,
+				err_msg = process_included_auth_file(inc_filename, false,
+										  filename, depth + 1, elevel,
 										  tok_lines);
 
 				if (!err_msg)
@@ -780,8 +780,6 @@ process_line:
 next_line:
 		line_number += continuations + 1;
 	}
-
-	MemoryContextSwitchTo(oldcxt);
 }
 
 /*
@@ -2650,9 +2648,9 @@ open_inc_file(HbaIncludeKind kind, const char *inc_filename, bool strict,
  * Returns NULL if no error happens during tokenization, otherwise the error.
  */
 static char *
-process_included_authfile(const char *inc_filename, bool strict,
-						  const char *outer_filename, int depth, int elevel,
-						  MemoryContext linecxt, List **tok_lines)
+process_included_auth_file(const char *inc_filename, bool strict,
+						   const char *outer_filename, int depth, int elevel,
+						   List **tok_lines)
 {
 	char	   *inc_fullname;
 	FILE	   *inc_file;
@@ -2693,8 +2691,8 @@ process_included_authfile(const char *inc_filename, bool strict,
 		Assert(err_msg == NULL);
 	}
 
-	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
-							   tok_lines, depth, elevel);
+	tokenize_auth_file_internal(inc_fullname, inc_file,
+								tok_lines, depth, elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
diff --git a/src/backend/utils/misc/Makefile b/src/backend/utils/misc/Makefile
index 6097309033..b9ee4eb48a 100644
--- a/src/backend/utils/misc/Makefile
+++ b/src/backend/utils/misc/Makefile
@@ -15,6 +15,7 @@ include $(top_builddir)/src/Makefile.global
 override CPPFLAGS := -I. -I$(srcdir) $(CPPFLAGS)
 
 OBJS = \
+	conffiles.o \
 	guc.o \
 	guc-file.o \
 	guc_funcs.o \
diff --git a/src/backend/utils/misc/conffiles.c b/src/backend/utils/misc/conffiles.c
new file mode 100644
index 0000000000..c9659a7c70
--- /dev/null
+++ b/src/backend/utils/misc/conffiles.c
@@ -0,0 +1,161 @@
+/*--------------------------------------------------------------------
+ * conffiles.c
+ *
+ * Utilities and tools related to the handling of configuration files.
+ *
+ * This file contains the generic tools to work on configuration files
+ * used by PostgreSQL, be they related to GUC, HBA or ident files.
+ *
+ *
+ * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/misc/conffiles.c
+ *
+ *--------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include <dirent.h>
+
+#include "common/file_utils.h"
+#include "miscadmin.h"
+#include "storage/fd.h"
+#include "utils/conffiles.h"
+
+/*
+ * Given a configuration file or directory location that may be a relative
+ * path, return an absolute one.  We consider the location to be relative to
+ * the directory holding the calling file, or to DataDir if no calling file.
+ */
+char *
+AbsoluteConfigLocation(const char *location, const char *calling_file)
+{
+	char		abs_path[MAXPGPATH];
+
+	if (is_absolute_path(location))
+		return pstrdup(location);
+	else
+	{
+		if (calling_file != NULL)
+		{
+			strlcpy(abs_path, calling_file, sizeof(abs_path));
+			get_parent_directory(abs_path);
+			join_path_components(abs_path, abs_path, location);
+			canonicalize_path(abs_path);
+		}
+		else
+		{
+			Assert(DataDir);
+			join_path_components(abs_path, DataDir, location);
+			canonicalize_path(abs_path);
+		}
+		return pstrdup(abs_path);
+	}
+}
+
+
+/*
+ * Returns the list of config files located in a directory, in alphabetical
+ * order.
+ *
+ * On error, returns NULL with details stored in "err_msg".
+ */
+char **
+GetConfFilesInDir(const char *includedir, const char *calling_file,
+				  int elevel, int *num_filenames, char **err_msg)
+{
+	char	   *directory;
+	DIR		   *d;
+	struct dirent *de;
+	char	  **filenames = NULL;
+	int			size_filenames;
+
+	/*
+	 * Reject directory name that is all-blank (including empty), as that
+	 * leads to confusion --- we'd read the containing directory, typically
+	 * resulting in recursive inclusion of the same file(s).
+	 */
+	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty configuration directory name: \"%s\"",
+						includedir)));
+		*err_msg = "empty configuration directory name";
+		return NULL;
+	}
+
+	directory = AbsoluteConfigLocation(includedir, calling_file);
+	d = AllocateDir(directory);
+	if (d == NULL)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration directory \"%s\": %m",
+						directory)));
+		*err_msg = psprintf("could not open directory \"%s\"", directory);
+		goto cleanup;
+	}
+
+	/*
+	 * Read the directory and put the filenames in an array, so we can sort
+	 * them prior to caller processing the contents.
+	 */
+	size_filenames = 32;
+	filenames = (char **) palloc(size_filenames * sizeof(char *));
+	*num_filenames = 0;
+
+	while ((de = ReadDir(d, directory)) != NULL)
+	{
+		PGFileType	de_type;
+		char		filename[MAXPGPATH];
+
+		/*
+		 * Only parse files with names ending in ".conf".  Explicitly reject
+		 * files starting with ".".  This excludes things like "." and "..",
+		 * as well as typical hidden files, backup files, and editor debris.
+		 */
+		if (strlen(de->d_name) < 6)
+			continue;
+		if (de->d_name[0] == '.')
+			continue;
+		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+			continue;
+
+		join_path_components(filename, directory, de->d_name);
+		canonicalize_path(filename);
+		de_type = get_dirent_type(filename, de, true, elevel);
+		if (de_type == PGFILETYPE_ERROR)
+		{
+			*err_msg = psprintf("could not stat file \"%s\"", filename);
+			pfree(filenames);
+			filenames = NULL;
+			goto cleanup;
+		}
+		else if (de_type != PGFILETYPE_DIR)
+		{
+			/* Add file to array, increasing its size in blocks of 32 */
+			if (*num_filenames >= size_filenames)
+			{
+				size_filenames += 32;
+				filenames = (char **) repalloc(filenames,
+										size_filenames * sizeof(char *));
+			}
+			filenames[*num_filenames] = pstrdup(filename);
+			(*num_filenames)++;
+		}
+	}
+
+	/* Sort the files by name before leaving */
+	if (*num_filenames > 0)
+		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
+
+cleanup:
+	if (d)
+		FreeDir(d);
+	pfree(directory);
+	return filenames;
+}
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index f76a56398b..937dc5fbf8 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -18,6 +18,7 @@
 #include "miscadmin.h"
 #include "storage/fd.h"
 #include <sys/stat.h>
+#include "utils/conffiles.h"
 #include "utils/memutils.h"
 }
 
@@ -156,37 +157,6 @@ ProcessConfigFile(GucContext context)
 	MemoryContextDelete(config_cxt);
 }
 
-/*
- * Given a configuration file or directory location that may be a relative
- * path, return an absolute one.  We consider the location to be relative to
- * the directory holding the calling file, or to DataDir if no calling file.
- */
-static char *
-AbsoluteConfigLocation(const char *location, const char *calling_file)
-{
-	char		abs_path[MAXPGPATH];
-
-	if (is_absolute_path(location))
-		return pstrdup(location);
-	else
-	{
-		if (calling_file != NULL)
-		{
-			strlcpy(abs_path, calling_file, sizeof(abs_path));
-			get_parent_directory(abs_path);
-			join_path_components(abs_path, abs_path, location);
-			canonicalize_path(abs_path);
-		}
-		else
-		{
-			Assert(DataDir);
-			join_path_components(abs_path, DataDir, location);
-			canonicalize_path(abs_path);
-		}
-		return pstrdup(abs_path);
-	}
-}
-
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -345,110 +315,6 @@ GUC_flex_fatal(const char *msg)
 	return 0;					/* keep compiler quiet */
 }
 
-/*
- * Returns the list of config files located in a directory, in alphabetical
- * order.
- *
- * We don't check for recursion or too-deep nesting depth here, its up to the
- * caller to take care of that.
- */
-char **
-GetDirConfFiles(const char *includedir, const char *calling_file, int elevel,
-				int *num_filenames, char **err_msg)
-{
-	char	   *directory;
-	DIR		   *d;
-	struct dirent *de;
-	char	  **filenames;
-	int			size_filenames;
-
-	/*
-	 * Reject directory name that is all-blank (including empty), as that
-	 * leads to confusion --- we'd read the containing directory, typically
-	 * resulting in recursive inclusion of the same file(s).
-	 */
-	if (strspn(includedir, " \t\r\n") == strlen(includedir))
-	{
-		ereport(elevel,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("empty configuration directory name: \"%s\"",
-						includedir)));
-		*err_msg = "empty configuration directory name";
-		return NULL;
-	}
-
-	directory = AbsoluteConfigLocation(includedir, calling_file);
-	d = AllocateDir(directory);
-	if (d == NULL)
-	{
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration directory \"%s\": %m",
-						directory)));
-		*err_msg = psprintf("could not open directory \"%s\"", directory);
-		filenames = NULL;
-		goto cleanup;
-	}
-
-	/*
-	 * Read the directory and put the filenames in an array, so we can sort
-	 * them prior to caller processing the contents.
-	 */
-	size_filenames = 32;
-	filenames = (char **) palloc(size_filenames * sizeof(char *));
-	*num_filenames = 0;
-
-	while ((de = ReadDir(d, directory)) != NULL)
-	{
-		PGFileType	de_type;
-		char		filename[MAXPGPATH];
-
-		/*
-		 * Only parse files with names ending in ".conf".  Explicitly reject
-		 * files starting with ".".  This excludes things like "." and "..",
-		 * as well as typical hidden files, backup files, and editor debris.
-		 */
-		if (strlen(de->d_name) < 6)
-			continue;
-		if (de->d_name[0] == '.')
-			continue;
-		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
-			continue;
-
-		join_path_components(filename, directory, de->d_name);
-		canonicalize_path(filename);
-		de_type = get_dirent_type(filename, de, true, elevel);
-		if (de_type == PGFILETYPE_ERROR)
-		{
-			*err_msg = psprintf("could not stat file \"%s\"", filename);
-			pfree(filenames);
-			filenames = NULL;
-			goto cleanup;
-		}
-		else if (de_type != PGFILETYPE_DIR)
-		{
-			/* Add file to array, increasing its size in blocks of 32 */
-			if (*num_filenames >= size_filenames)
-			{
-				size_filenames += 32;
-				filenames = (char **) repalloc(filenames,
-										size_filenames * sizeof(char *));
-			}
-			filenames[*num_filenames] = pstrdup(filename);
-			(*num_filenames)++;
-		}
-	}
-
-	if (*num_filenames > 0)
-		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
-
-cleanup:
-	if (d)
-		FreeDir(d);
-	pfree(directory);
-	return filenames;
-}
-
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -714,7 +580,7 @@ ParseConfigDirectory(const char *includedir,
 	char	  **filenames;
 	int			num_filenames;
 
-	filenames = GetDirConfFiles(includedir, calling_file, elevel,
+	filenames = GetConfFilesInDir(includedir, calling_file, elevel,
 							   &num_filenames, &err_msg);
 
 	if (!filenames)
diff --git a/src/backend/utils/misc/meson.build b/src/backend/utils/misc/meson.build
index db4de225e1..e7a9730229 100644
--- a/src/backend/utils/misc/meson.build
+++ b/src/backend/utils/misc/meson.build
@@ -1,4 +1,5 @@
 backend_sources += files(
+  'conffiles.c',
   'guc.c',
   'guc_funcs.c',
   'guc_tables.c',
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index c2b48c43c9..cfc23fa213 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -7,6 +7,7 @@ tests += {
       't/001_password.pl',
       't/002_saslprep.pl',
       't/003_peer.pl',
+      't/004_file_inclusion.pl',
     ],
   },
 }
-- 
2.38.1

v15-0001-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/x-diff; charset=us-asciiDownload
From 2b727d7f011b94952afda81e94b369dc2d60a98f Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 2 Nov 2022 16:42:53 +0900
Subject: [PATCH v15 1/2] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/include/libpq/hba.h                       |   3 +-
 src/include/utils/guc.h                       |   2 +
 src/backend/libpq/hba.c                       | 378 ++++++++--
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  43 +-
 src/backend/utils/misc/guc-file.l             | 229 +++---
 .../authentication/t/004_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   1 +
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  23 +-
 12 files changed, 1253 insertions(+), 221 deletions(-)
 create mode 100644 src/test/authentication/t/004_file_inclusion.pl

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 20f5aa56ea..a1d9bef0e9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,16 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{map_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{map_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 7ad227d34a..0ac14b370c 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -178,6 +178,7 @@ extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int depth,
+										int elevel);
 
 #endif							/* HBA_H */
diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h
index b3aaff9665..59ca39d908 100644
--- a/src/include/utils/guc.h
+++ b/src/include/utils/guc.h
@@ -144,6 +144,8 @@ typedef struct ConfigVariable
 	struct ConfigVariable *next;
 } ConfigVariable;
 
+extern char **GetDirConfFiles(const char *includedir, const char *calling_file,
+							  int elevel, int *num_filenames, char **err_msg);
 extern bool ParseConfigFile(const char *config_file, bool strict,
 							const char *calling_file, int calling_lineno,
 							int depth, int elevel,
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index e9fc0af7c9..1034ff8220 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -22,6 +22,7 @@
 #include <sys/param.h>
 #include <sys/socket.h>
 #include <netdb.h>
+#include <sys/stat.h>
 #include <netinet/in.h>
 #include <arpa/inet.h>
 #include <unistd.h>
@@ -70,6 +71,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -115,14 +122,26 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int depth, int elevel,
+							   char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 							   char **err_msg, int elevel);
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
+static FILE *open_inc_file(HbaIncludeKind kind, const char *inc_filename,
+						   bool strict, const char *outer_filename, int elevel,
+						   char **err_msg, char **inc_fullname);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -413,7 +432,7 @@ regexec_auth_token(const char *match, AuthToken *token, size_t nmatch,
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int depth, int elevel, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -429,7 +448,7 @@ next_field_expand(const char *filename, char **lineptr,
 
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
-			tokens = tokenize_inc_file(tokens, filename, buf + 1,
+			tokens = tokenize_inc_file(tokens, filename, buf + 1, depth + 1,
 									   elevel, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
@@ -457,6 +476,7 @@ static List *
 tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
+				  int depth,
 				  int elevel,
 				  char **err_msg)
 {
@@ -466,39 +486,30 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
 	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
-
-	inc_file = AllocateFile(inc_fullname, "r");
-	if (inc_file == NULL)
-	{
-		int			save_errno = errno;
-
+		*err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
 		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
-		pfree(inc_fullname);
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", *err_msg)));
 		return tokens;
 	}
 
+	inc_file = open_inc_file(SecondaryAuthFile, inc_filename, true,
+							 outer_filename, elevel, err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+		return tokens;
+
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, depth + 1,
+								 elevel);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -536,11 +547,38 @@ tokenize_inc_file(List *tokens,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -549,30 +587,22 @@ tokenize_inc_file(List *tokens,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int depth, int elevel)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -625,7 +655,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		{
 			List	   *current_field;
 
-			current_field = next_field_expand(filename, &lineptr,
+			current_field = next_field_expand(filename, &lineptr, depth,
 											  elevel, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
@@ -633,30 +663,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedAuthLine *tok_line;
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->file_name = pstrdup(filename);
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetDirConfFiles(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -2355,7 +2482,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2445,6 +2572,135 @@ load_hba(void)
 	return true;
 }
 
+/*
+ * Open the  given file for inclusion in an authentication file, whether
+ * secondary or included.
+ */
+static FILE *
+open_inc_file(HbaIncludeKind kind, const char *inc_filename, bool strict,
+			  const char *outer_filename, int elevel, char **err_msg,
+			  char **inc_fullname)
+{
+	FILE	   *inc_file;
+
+	if (is_absolute_path(inc_filename))
+	{
+		/* absolute path is taken as-is */
+		*inc_fullname = pstrdup(inc_filename);
+	}
+	else
+	{
+		/* relative path is relative to dir of calling file */
+		*inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
+									   strlen(inc_filename) + 1);
+		strcpy(*inc_fullname, outer_filename);
+		get_parent_directory(*inc_fullname);
+		join_path_components(*inc_fullname, *inc_fullname, inc_filename);
+		canonicalize_path(*inc_fullname);
+	}
+
+	inc_file = AllocateFile(*inc_fullname, "r");
+	if (inc_file == NULL)
+	{
+		int			save_errno = errno;
+		const char *msglog;
+		const char *msgview;
+
+		if (strict)
+		{
+			switch (kind)
+			{
+				case SecondaryAuthFile:
+					msglog = "could not open secondary authentication file \"@%s\" as \"%s\": %m";
+					msgview = "could not open secondary authentication file \"@%s\" as \"%s\": %s";
+					break;
+				case IncludedAuthFile:
+					msglog = "could not open included authentication file \"%s\" as \"%s\": %m";
+					msgview = "could not open included authentication file \"%s\" as \"%s\": %s";
+					break;
+				default:
+					elog(ERROR, "unknown HbaIncludeKind: %d", kind);
+					break;
+			}
+
+			ereport(elevel,
+					(errcode_for_file_access(),
+					 errmsg(msglog, inc_filename, *inc_fullname)));
+			*err_msg = psprintf(msgview, inc_filename, *inc_fullname,
+								strerror(save_errno));
+		}
+		else
+		{
+			Assert(kind == IncludedAuthFile);
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							*inc_fullname)));
+		}
+
+		pfree(*inc_fullname);
+		*inc_fullname = NULL;
+		return NULL;
+	}
+
+	return inc_file;
+}
+
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		err_msg = psprintf("could not open configuration file \"%s\": maximum nesting depth exceeded",
+							inc_filename);
+		ereport(elevel,
+				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
+				 errmsg("%s", err_msg)));
+		return err_msg;
+	}
+
+	inc_file = open_inc_file(IncludedAuthFile, inc_filename, strict,
+							 outer_filename, elevel, &err_msg, &inc_fullname);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_inc_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+			return NULL;
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, depth, elevel);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
 
 /*
  * Parse one tokenised line from the ident config file and store the result in
@@ -2727,7 +2983,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, LOG);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index e12ff8ca72..6207ba2456 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int map_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int map_number, char *filename, int lineno,
+							IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, char *filename,int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -203,6 +204,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(rule_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -346,7 +350,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -387,7 +391,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open configuration file \"%s\": %m",
 						HbaFileName)));
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -409,7 +413,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			rule_number++;
 
 		fill_hba_line(tuple_store, tupdesc, rule_number,
-					  tok_line->line_num, hbaline, tok_line->err_msg);
+					  tok_line->file_name, tok_line->line_num, hbaline,
+					  tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -446,7 +451,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -455,7 +460,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * map_number: unique identifier among all valid maps
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -464,7 +470,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int map_number, int lineno, IdentLine *ident,
+				int map_number, char *filename, int lineno, IdentLine *ident,
 				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -484,6 +490,9 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(map_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -496,7 +505,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -536,7 +545,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 				 errmsg("could not open usermap file \"%s\": %m",
 						IdentFileName)));
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, 0, DEBUG3);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -558,8 +567,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			map_number++;
 
 		fill_ident_line(tuple_store, tupdesc, map_number,
-						tok_line->line_num, identline,
-						tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num,
+						identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index 2436396306..f76a56398b 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -345,6 +345,110 @@ GUC_flex_fatal(const char *msg)
 	return 0;					/* keep compiler quiet */
 }
 
+/*
+ * Returns the list of config files located in a directory, in alphabetical
+ * order.
+ *
+ * We don't check for recursion or too-deep nesting depth here, its up to the
+ * caller to take care of that.
+ */
+char **
+GetDirConfFiles(const char *includedir, const char *calling_file, int elevel,
+				int *num_filenames, char **err_msg)
+{
+	char	   *directory;
+	DIR		   *d;
+	struct dirent *de;
+	char	  **filenames;
+	int			size_filenames;
+
+	/*
+	 * Reject directory name that is all-blank (including empty), as that
+	 * leads to confusion --- we'd read the containing directory, typically
+	 * resulting in recursive inclusion of the same file(s).
+	 */
+	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	{
+		ereport(elevel,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("empty configuration directory name: \"%s\"",
+						includedir)));
+		*err_msg = "empty configuration directory name";
+		return NULL;
+	}
+
+	directory = AbsoluteConfigLocation(includedir, calling_file);
+	d = AllocateDir(directory);
+	if (d == NULL)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open configuration directory \"%s\": %m",
+						directory)));
+		*err_msg = psprintf("could not open directory \"%s\"", directory);
+		filenames = NULL;
+		goto cleanup;
+	}
+
+	/*
+	 * Read the directory and put the filenames in an array, so we can sort
+	 * them prior to caller processing the contents.
+	 */
+	size_filenames = 32;
+	filenames = (char **) palloc(size_filenames * sizeof(char *));
+	*num_filenames = 0;
+
+	while ((de = ReadDir(d, directory)) != NULL)
+	{
+		PGFileType	de_type;
+		char		filename[MAXPGPATH];
+
+		/*
+		 * Only parse files with names ending in ".conf".  Explicitly reject
+		 * files starting with ".".  This excludes things like "." and "..",
+		 * as well as typical hidden files, backup files, and editor debris.
+		 */
+		if (strlen(de->d_name) < 6)
+			continue;
+		if (de->d_name[0] == '.')
+			continue;
+		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
+			continue;
+
+		join_path_components(filename, directory, de->d_name);
+		canonicalize_path(filename);
+		de_type = get_dirent_type(filename, de, true, elevel);
+		if (de_type == PGFILETYPE_ERROR)
+		{
+			*err_msg = psprintf("could not stat file \"%s\"", filename);
+			pfree(filenames);
+			filenames = NULL;
+			goto cleanup;
+		}
+		else if (de_type != PGFILETYPE_DIR)
+		{
+			/* Add file to array, increasing its size in blocks of 32 */
+			if (*num_filenames >= size_filenames)
+			{
+				size_filenames += 32;
+				filenames = (char **) repalloc(filenames,
+										size_filenames * sizeof(char *));
+			}
+			filenames[*num_filenames] = pstrdup(filename);
+			(*num_filenames)++;
+		}
+	}
+
+	if (*num_filenames > 0)
+		qsort(filenames, *num_filenames, sizeof(char *), pg_qsort_strcmp);
+
+cleanup:
+	if (d)
+		FreeDir(d);
+	pfree(directory);
+	return filenames;
+}
+
 /*
  * Read and parse a single configuration file.  This function recurses
  * to handle "include" directives.
@@ -606,127 +710,30 @@ ParseConfigDirectory(const char *includedir,
 					 ConfigVariable **head_p,
 					 ConfigVariable **tail_p)
 {
-	char	   *directory;
-	DIR		   *d;
-	struct dirent *de;
+	char	   *err_msg;
 	char	  **filenames;
 	int			num_filenames;
-	int			size_filenames;
-	bool		status;
 
-	/*
-	 * Reject directory name that is all-blank (including empty), as that
-	 * leads to confusion --- we'd read the containing directory, typically
-	 * resulting in recursive inclusion of the same file(s).
-	 */
-	if (strspn(includedir, " \t\r\n") == strlen(includedir))
+	filenames = GetDirConfFiles(includedir, calling_file, elevel,
+							   &num_filenames, &err_msg);
+
+	if (!filenames)
 	{
-		ereport(elevel,
-				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
-				 errmsg("empty configuration directory name: \"%s\"",
-						includedir)));
-		record_config_file_error("empty configuration directory name",
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
+		record_config_file_error(err_msg, calling_file, calling_lineno, head_p,
+								 tail_p);
 		return false;
 	}
 
-	/*
-	 * We don't check for recursion or too-deep nesting depth here; the
-	 * subsequent calls to ParseConfigFile will take care of that.
-	 */
-
-	directory = AbsoluteConfigLocation(includedir, calling_file);
-	d = AllocateDir(directory);
-	if (d == NULL)
+	for (int i = 0; i < num_filenames; i++)
 	{
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration directory \"%s\": %m",
-						directory)));
-		record_config_file_error(psprintf("could not open directory \"%s\"",
-										  directory),
-								 calling_file, calling_lineno,
-								 head_p, tail_p);
-		status = false;
-		goto cleanup;
+		if (!ParseConfigFile(filenames[i], true,
+							 calling_file, calling_lineno,
+							 depth, elevel,
+							 head_p, tail_p))
+			return false;
 	}
 
-	/*
-	 * Read the directory and put the filenames in an array, so we can sort
-	 * them prior to processing the contents.
-	 */
-	size_filenames = 32;
-	filenames = (char **) palloc(size_filenames * sizeof(char *));
-	num_filenames = 0;
-
-	while ((de = ReadDir(d, directory)) != NULL)
-	{
-		PGFileType	de_type;
-		char		filename[MAXPGPATH];
-
-		/*
-		 * Only parse files with names ending in ".conf".  Explicitly reject
-		 * files starting with ".".  This excludes things like "." and "..",
-		 * as well as typical hidden files, backup files, and editor debris.
-		 */
-		if (strlen(de->d_name) < 6)
-			continue;
-		if (de->d_name[0] == '.')
-			continue;
-		if (strcmp(de->d_name + strlen(de->d_name) - 5, ".conf") != 0)
-			continue;
-
-		join_path_components(filename, directory, de->d_name);
-		canonicalize_path(filename);
-		de_type = get_dirent_type(filename, de, true, elevel);
-		if (de_type == PGFILETYPE_ERROR)
-		{
-			record_config_file_error(psprintf("could not stat file \"%s\"",
-											  filename),
-									 calling_file, calling_lineno,
-									 head_p, tail_p);
-			status = false;
-			goto cleanup;
-		}
-		else if (de_type != PGFILETYPE_DIR)
-		{
-			/* Add file to array, increasing its size in blocks of 32 */
-			if (num_filenames >= size_filenames)
-			{
-				size_filenames += 32;
-				filenames = (char **) repalloc(filenames,
-											   size_filenames * sizeof(char *));
-			}
-			filenames[num_filenames] = pstrdup(filename);
-			num_filenames++;
-		}
-	}
-
-	if (num_filenames > 0)
-	{
-		int			i;
-
-		qsort(filenames, num_filenames, sizeof(char *), pg_qsort_strcmp);
-		for (i = 0; i < num_filenames; i++)
-		{
-			if (!ParseConfigFile(filenames[i], true,
-								 calling_file, calling_lineno,
-								 depth, elevel,
-								 head_p, tail_p))
-			{
-				status = false;
-				goto cleanup;
-			}
-		}
-	}
-	status = true;
-
-cleanup:
-	if (d)
-		FreeDir(d);
-	pfree(directory);
-	return status;
+	return true;
 }
 
 /*
diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
new file mode 100644
index 0000000000..ebc883a842
--- /dev/null
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open secondary authentication file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open included authentication file "\.\.\/not_a_file" as "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open secondary authentication file \"\@dbnames_fails.conf\" as \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	'could not open configuration file "recurse.conf": maximum nesting depth exceeded');
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open secondary authentication file \"\@osnames_fails.conf\" as \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	'could not open configuration file "ident_recurse.conf": maximum nesting depth exceeded');
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open included authentication file \"../not_a_file\" as \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 624d0e5aae..90162b0fa9 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..2ae723de66 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -863,8 +914,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -875,6 +928,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 947598e7e7..3ee8df5974 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1002,12 +1002,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1152,12 +1161,22 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this map
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this map in <filename>pg_ident.conf</filename>
+       Line number of this map in the corresponding
+       <literal>file_name</literal>
       </para></entry>
      </row>
 
-- 
2.38.1

#67Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#66)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Nov 02, 2022 at 04:46:48PM +0900, Michael Paquier wrote:

On Fri, Oct 28, 2022 at 11:49:54AM +0800, Julien Rouhaud wrote:

To be honest I'd rather not to. It's excessively annoying to work on those
tests (I spent multiple days trying to make it as clean and readable as
possible), and splitting it to only test the current infrastructure will need
some substantial efforts.

Well, I'd really like to split things as much as possible, beginning
with some solid basics before extending its capabilities. This
reduces the odds of introducing issues in-between, particularly in
areas as sensible as authentication that involves not-yet-logged-in
users.

But more importantly, the next commit that will add tests for file inclusion
will then be totally unmaintainable and unreadable, so that's IMO even worse.
I think it will probably either be the current file overwritten or a new one
written from scratch if some changes are done in the simplified test, and I'm
not volunteering to do that.

Not sure about that either.

Maybe one alternative approach would be to keep modifying the current test, and
only do the split when committing the first part? The split itself should be
easy and mechanical: just remove everything unneeded (different file names
including the file name argument from the add_(hba|ident)_line, inclusion
directive and so on). Even if the diff is then difficult to read it's not
really a problem as it should have already been reviewed.

Anyway, the last patch posted on this thread does not apply and the CF
bot fails, so it needed a rebase. I have first noticed that your
patch included some doc fixes independent of this thread, so I have
applied that as e765028 and backpatched it down to 15.

Yes I saw that this morning, thanks!

The TAP test
needs to be renamed to 0004, and it was missing from meson.build,
hence the CI was not testing it.

Ah right. This is quite annoying that we have to explicitly name every single
test file there. For the source code it's hard to not notice a missed file and
you will get an error in the CI, but a missed test is just entirely transparent
:(

I have spent some time starting at the logic today of the whole, and
GetConfFilesInDir() is really the first thing that stood out. I am
not sure that it makes much sense to keep that in guc-file.c, TBH,
once we feed it into hba.c. Perhaps we could put the refactored
routine (and AbsoluteConfigLocation as a side effect) into a new file
in misc/?

Yes I was wondering about it too. A new fine in misc/ looks sensible.

As of HEAD, tokenize_inc_file() is the place where we handle a list of
tokens included with an '@' file, appending the existing set of
AuthTokens into the list we are building by grabbing a copy of these
before deleting the line memory context.

Your patch proposes a different alternative, which is to pass down the
memory context created in tokenize_auth_file() down to the callers
with tokenize_file_with_context() dealing with all the internals.
process_included_auth_file() is different extension of that.
This may come down to a matter of taste, but could be be cleaner to
take an approach similar to tokenize_inc_file() and just create a copy
of the AuthToken list coming from a full file and append it to the
result rather than passing around the memory context for the lines?
This would lead to some simplifications, it seems, at least with the
number of arguments passed down across the layers.

I guess it goes in the same direction as 8fea86830e1 where infrastructure to
copy AuthTokens was added, I'm fine either way.

The addition of a check for the depth in two places seems unnecessary,
and it looks like we should do this kind of check in only one place.

I usually prefer to add a maybe unnecessary depth check since it's basically
free rather than risking an unfriendly stack size error (including in possible
later refactoring), but no objection to get rid of it.

I have not tried yet, but if we actually move the AllocateFile() and
FreeFile() calls within tokenize_auth_file(), it looks like we may be
able to get to a simpler logic without the need of the with_context()
flavor (even no process_included_auth_file required)? That could make
the interface easier to follow as a whole, while limiting the presence
of AllocateFile() and FreeFile() to a single code path, impacting
open_inc_file() that relies on what the patch uses for
SecondaryAuthFile and IncludedAuthFile (we could attempt to use the
same error message everywhere as well, as one could expect that
expanded and included files have different names which is enough to
guess which one is an inclusion and which one is a secondary).

You meant tokenize_auth_file_internal? Yes possibly, in general I tried
to avoid changing too much the existing code to ease the patch acceptance, but
I agree it would make things simpler.

Attached are two patches: the first one is a rebase of what you have
posted, and the second one are some changes I did while playing with
the logic. In the second one, except for conffiles.{h.c}, the changes
are just a POC but that's to show the areas that I am planning to
rework, and your tests pass with it. I still need to think about all
that and reconsider the design of the interface that would fit with
the tokenization of the inclusions, without that many subroutines to
do the work as it makes the code harder to follow. Well, that's to
say that I am not staying idle :)

Thanks!

#68Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#67)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Nov 02, 2022 at 09:06:02PM +0800, Julien Rouhaud wrote:

Maybe one alternative approach would be to keep modifying the current test, and
only do the split when committing the first part? The split itself should be
easy and mechanical: just remove everything unneeded (different file names
including the file name argument from the add_(hba|ident)_line, inclusion
directive and so on). Even if the diff is then difficult to read it's not
really a problem as it should have already been reviewed.

I have not reviewed the test part much yet, TBH. The path
manipulations because of WIN32 are a bit annoying. I was wondering if
we could avoid all that by using the basenames, as one option.

I have spent some time starting at the logic today of the whole, and
GetConfFilesInDir() is really the first thing that stood out. I am
not sure that it makes much sense to keep that in guc-file.c, TBH,
once we feed it into hba.c. Perhaps we could put the refactored
routine (and AbsoluteConfigLocation as a side effect) into a new file
in misc/?

Yes I was wondering about it too. A new fine in misc/ looks sensible.

conffiles.c is the best thing I could come up with, conf.c and
config.c being too generic and these are routines that work on
configuration files, so..

Your patch proposes a different alternative, which is to pass down the
memory context created in tokenize_auth_file() down to the callers
with tokenize_file_with_context() dealing with all the internals.
process_included_auth_file() is different extension of that.
This may come down to a matter of taste, but could be be cleaner to
take an approach similar to tokenize_inc_file() and just create a copy
of the AuthToken list coming from a full file and append it to the
result rather than passing around the memory context for the lines?
This would lead to some simplifications, it seems, at least with the
number of arguments passed down across the layers.

I guess it goes in the same direction as 8fea86830e1 where infrastructure to
copy AuthTokens was added, I'm fine either way.

I won't hide that I am trying to make the changes a maximum
incremental for this thread so as the final part is easy-ish.

The addition of a check for the depth in two places seems unnecessary,
and it looks like we should do this kind of check in only one place.

I usually prefer to add a maybe unnecessary depth check since it's basically
free rather than risking an unfriendly stack size error (including in possible
later refactoring), but no objection to get rid of it.

The depth check is a good idea, though I guess that there is an
argument for having it in only one code path, not two.

I have not tried yet, but if we actually move the AllocateFile() and
FreeFile() calls within tokenize_auth_file(), it looks like we may be
able to get to a simpler logic without the need of the with_context()
flavor (even no process_included_auth_file required)? That could make
the interface easier to follow as a whole, while limiting the presence
of AllocateFile() and FreeFile() to a single code path, impacting
open_inc_file() that relies on what the patch uses for
SecondaryAuthFile and IncludedAuthFile (we could attempt to use the
same error message everywhere as well, as one could expect that
expanded and included files have different names which is enough to
guess which one is an inclusion and which one is a secondary).

You meant tokenize_auth_file_internal? Yes possibly, in general I tried
to avoid changing too much the existing code to ease the patch acceptance, but
I agree it would make things simpler.

I *guess* that my mind implied tokenize_auth_file() as of
yesterday's.
--
Michael

#69Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#68)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Thu, Nov 03, 2022 at 08:55:05PM +0900, Michael Paquier wrote:

On Wed, Nov 02, 2022 at 09:06:02PM +0800, Julien Rouhaud wrote:

The addition of a check for the depth in two places seems unnecessary,
and it looks like we should do this kind of check in only one place.

I usually prefer to add a maybe unnecessary depth check since it's basically
free rather than risking an unfriendly stack size error (including in possible
later refactoring), but no objection to get rid of it.

The depth check is a good idea, though I guess that there is an
argument for having it in only one code path, not two.

So, I have looked at this specific part, and I think that we should
try to solve this issue on its own as the error message leading to a
failure at startup is confusing now, giving:
FATAL: exceeded maxAllocatedDescs (166) while trying to open file \"blah\".

It took me some time to ponder about what kind of interface we should
use for this job, but I think that it comes down to three things:
- The depth ought to be checked just before doing AllocateFile() for a
file.
- We should have one code path calling AllocateFile().
- The same interface should be used for hba.c and hbafuncs.c.

I have settled down to the following in the attached patch 0002:
open_auth_file(const char *filename, int elevel, int depth, char
**err_msg)

More checks could be added in this code path (guc-file.l does more
with immediate recursions, for example), but the depth level should be
enough anyway. I am aware of the fact that this reduces a bit the
information provided for what's called a "secondary" file in your
patch set, but I tend toward moving into something that's
minimalistic. Currently, we provide the file and its '@' shortcut,
and we don't mention at all the file where it comes from. This is a
minimal problem now because we only have pg_hba.conf to worry about
for most users, still I am wondering whether it would be better in the
long run to complete that with an error context that mentions the
file(s) where this comes from and report a full chain of the events
that led to this opening failure. At the end, I think that 0002 moves
us closer toward the goal of this thread. Not been a fan of
HbaIncludeKind since the patch was proposed, TBH, as well.

Another thing is that this removes the need for open_inc_file(), my
opinion is that it is overcomplicated, and actually it seems like
under the non-strict mode (aka include_if_exists), we would generate a
LOG to mention that a configuration file is skipped, but there would
be nothing in err_msg for the TokenizedAuthLine, which is a mistake I
guess?

I have not tried yet, but if we actually move the AllocateFile() and
FreeFile() calls within tokenize_auth_file(), it looks like we may be
able to get to a simpler logic without the need of the with_context()
flavor (even no process_included_auth_file required)? That could make
the interface easier to follow as a whole, while limiting the presence
of AllocateFile() and FreeFile() to a single code path, impacting
open_inc_file() that relies on what the patch uses for
SecondaryAuthFile and IncludedAuthFile (we could attempt to use the
same error message everywhere as well, as one could expect that
expanded and included files have different names which is enough to
guess which one is an inclusion and which one is a secondary).

You meant tokenize_auth_file_internal? Yes possibly, in general I tried
to avoid changing too much the existing code to ease the patch acceptance, but
I agree it would make things simpler.

I *guess* that my mind implied tokenize_auth_file() as of
yesterday's.

While thinking more about the logic, I saw that your approach to use
the same MemoryContext "linecxt" and pass it down to the various
layers with tokenize_file_with_context() has the advantage to remove
the need to copy a list of TokenizedAuthLine coming from other
included files and/or dirs, which could easily lead to bugs especially
for the sourcefile if someone is not careful enough. There is no need
to touch the existing tokenize_inc_file() that copies a set of
AuthTokens to an existing list in TokenizedAuthLine, of course.

Should we do without tokenize_file_with_context() though? There is an
argument for making everything go through tokenize_auth_file(), having
a MemoryContext as argument (aka if NULL create it, if not use it),
while still returning the memory context used? Similarly, it looks
like we should have no need for process_included_authfile() at the
end.

I have applied as a1a7bb8 the refactoring of the directory logic for
configuration files, and noticed that this could also be used for
tokenize_inc_file(). This reduces the whole patch by ~15%.

Attached is a set of three patches:
- 0001 changes tokenize_inc_file() to use AbsoluteConfigLocation().
AbsoluteConfigLocation() uses a static buffer and a MAXPGPATH, but
we'd rather change it to use a palloc()+strcpy() instead and remove
the static restriction? What do you think? The same applies for the
case where we use DataDir, actually, and it seems like there is no
point in this path-length restriction in this code path.
- 0002 invents the interface to open auth files and check for their
depths, simplifying the main patch a bit as there is no need to track
the depth level here and there anymore.
- 0003 is the rebased patch, simplified after the other changes. The
bulk of the patch is in its TAP test.
--
Michael

Attachments:

v16-0001-Expand-the-use-of-AbsoluteConfigLocation-in-hba..patchtext/x-diff; charset=us-asciiDownload
From 982af2a565e1a81362a1fc1893b2dbaf81049f2e Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 7 Nov 2022 12:55:39 +0900
Subject: [PATCH v16 1/3] Expand the use of AbsoluteConfigLocation() in hba.c

The logic in charge of expanding an include file for database and user
names used the same code as AbsoluteConfigLocation() when building the
configuration file to include, so simplify this code.
---
 src/backend/libpq/hba.c | 17 ++---------------
 1 file changed, 2 insertions(+), 15 deletions(-)

diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index e9fc0af7c9..a9f87ab5bf 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -41,6 +41,7 @@
 #include "storage/fd.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/conffiles.h"
 #include "utils/guc.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -466,21 +467,7 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
+	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
 
 	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
-- 
2.38.1

v16-0002-Invent-open_auth_file-in-hba.c-to-refactor-auth-.patchtext/x-diff; charset=us-asciiDownload
From 182379b35b738b45bb83f26774be1792176374c0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 7 Nov 2022 13:35:44 +0900
Subject: [PATCH v16 2/3] Invent open_auth_file() in hba.c, to refactor auth
 file opening

This adds a check on the recursion depth when including auth files,
something that has never been done when processing '@' files for
database and user name lists in pg_hba.conf.
---
 src/include/libpq/hba.h          |   4 +-
 src/backend/libpq/hba.c          | 100 ++++++++++++++++++++++---------
 src/backend/utils/adt/hbafuncs.c |  18 ++----
 3 files changed, 79 insertions(+), 43 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 7ad227d34a..a84a5f0961 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -177,7 +177,9 @@ extern int	check_usermap(const char *usermap_name,
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
+extern FILE *open_auth_file(const char *filename, int elevel, int depth,
+							char **err_msg);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int elevel, int depth);
 
 #endif							/* HBA_H */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index a9f87ab5bf..d8c0b585e5 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -117,7 +117,8 @@ static const char *const UserAuthName[] =
 
 
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int elevel,
+							   int depth, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
@@ -414,7 +415,7 @@ regexec_auth_token(const char *match, AuthToken *token, size_t nmatch,
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int elevel, int depth, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -431,7 +432,7 @@ next_field_expand(const char *filename, char **lineptr,
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
 			tokens = tokenize_inc_file(tokens, filename, buf + 1,
-									   elevel, err_msg);
+									   elevel, depth + 1, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
 	} while (trailing_comma && (*err_msg == NULL));
@@ -459,6 +460,7 @@ tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
 				  int elevel,
+				  int depth,
 				  char **err_msg)
 {
 	char	   *inc_fullname;
@@ -468,24 +470,18 @@ tokenize_inc_file(List *tokens,
 	MemoryContext linecxt;
 
 	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, err_msg);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
 	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
+		/* error already logged */
 		pfree(inc_fullname);
 		return tokens;
 	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel,
+								 depth);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -521,6 +517,59 @@ tokenize_inc_file(List *tokens,
 	return tokens;
 }
 
+/*
+ * open_auth_file
+ *		Open the given file.
+ *
+ * filename: the absolute path to the target file
+ * elevel: message logging level
+ * depth: recursion level of the file opened.
+ * err_msg: details about the error.
+ *
+ * Return value is the opened file.  On error, returns NULL with details
+ * about the error stored in "err_msg".
+ */
+FILE *
+open_auth_file(const char *filename, int elevel, int depth,
+			   char **err_msg)
+{
+	FILE	*file;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open file \"%s\": maximum nesting depth exceeded",
+						filename)));
+		if (err_msg)
+			*err_msg = psprintf("could not open file \"%s\": maximum nesting depth exceeded",
+								filename);
+		return NULL;
+	}
+
+	file = AllocateFile(filename, "r");
+	if (file == NULL)
+	{
+		int			save_errno = errno;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open file \"%s\": %m",
+						filename)));
+		if (err_msg)
+			*err_msg = psprintf("could not open file \"%s\": %s",
+								filename, strerror(save_errno));
+		return NULL;
+	}
+
+	return file;
+}
+
 /*
  * tokenize_auth_file
  *		Tokenize the given file.
@@ -532,6 +581,7 @@ tokenize_inc_file(List *tokens,
  * file: the already-opened target file
  * tok_lines: receives output list
  * elevel: message logging level
+ * depth: level of recursion when tokenizing the target file
  *
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
@@ -542,7 +592,7 @@ tokenize_inc_file(List *tokens,
  */
 MemoryContext
 tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+				   int elevel, int depth)
 {
 	int			line_number = 1;
 	StringInfoData buf;
@@ -613,7 +663,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 			List	   *current_field;
 
 			current_field = next_field_expand(filename, &lineptr,
-											  elevel, &err_msg);
+											  elevel, depth, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
 				current_line = lappend(current_line, current_field);
@@ -2332,17 +2382,14 @@ load_hba(void)
 	MemoryContext oldcxt;
 	MemoryContext hbacxt;
 
-	file = AllocateFile(HbaFileName, "r");
+	file = open_auth_file(HbaFileName, LOG, 0, NULL);
 	if (file == NULL)
 	{
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
+		/* error already logged */
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2703,18 +2750,15 @@ load_ident(void)
 	MemoryContext ident_context;
 	IdentLine  *newline;
 
-	file = AllocateFile(IdentFileName, "r");
+	/* not FATAL ... we just won't do any special ident maps */
+	file = open_auth_file(IdentFileName, LOG, 0, NULL);
 	if (file == NULL)
 	{
-		/* not fatal ... we just won't do any special ident maps */
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open usermap file \"%s\": %m",
-						IdentFileName)));
+		/* error already logged */
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index e12ff8ca72..b662e7b55f 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -380,14 +380,9 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 * (Most other error conditions should result in a message in a view
 	 * entry.)
 	 */
-	file = AllocateFile(HbaFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
+	file = open_auth_file(HbaFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -529,14 +524,9 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 * (Most other error conditions should result in a message in a view
 	 * entry.)
 	 */
-	file = AllocateFile(IdentFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open usermap file \"%s\": %m",
-						IdentFileName)));
+	file = open_auth_file(IdentFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
-- 
2.38.1

v16-0003-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/x-diff; charset=us-asciiDownload
From 2dacd37e619d5346f32656dcca070efdd68a00d4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 7 Nov 2022 14:23:00 +0900
Subject: [PATCH v16 3/3] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/backend/libpq/hba.c                       | 233 ++++++-
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  39 +-
 .../authentication/t/004_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   1 +
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  23 +-
 9 files changed, 1017 insertions(+), 74 deletions(-)
 create mode 100644 src/test/authentication/t/004_file_inclusion.pl

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 20f5aa56ea..a1d9bef0e9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,16 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{map_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{map_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index d8c0b585e5..bd38ee0ba6 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -71,6 +71,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -116,6 +122,10 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel,
 							   int depth, char **err_msg);
@@ -125,6 +135,10 @@ static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 							   char **err_msg, int elevel);
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -572,11 +586,38 @@ open_auth_file(const char *filename, int elevel, int depth,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -586,30 +627,22 @@ open_auth_file(const char *filename, int elevel, int depth,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel, int depth)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel, int depth)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -670,30 +703,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedAuthLine *tok_line;
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->file_name = pstrdup(filename);
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetConfFilesInDir(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -2480,6 +2610,53 @@ load_hba(void)
 }
 
 
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, &err_msg);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_auth_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+		{
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							inc_fullname)));
+			return NULL;
+		}
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, elevel, depth);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
+
 /*
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index b662e7b55f..f9c99d41c6 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int map_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int map_number, char *filename, int lineno,
+							IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, char *filename,int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -203,6 +204,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(rule_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -346,7 +350,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -404,7 +408,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			rule_number++;
 
 		fill_hba_line(tuple_store, tupdesc, rule_number,
-					  tok_line->line_num, hbaline, tok_line->err_msg);
+					  tok_line->file_name, tok_line->line_num, hbaline,
+					  tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -441,7 +446,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -450,7 +455,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * map_number: unique identifier among all valid maps
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -459,7 +465,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int map_number, int lineno, IdentLine *ident,
+				int map_number, char *filename, int lineno, IdentLine *ident,
 				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -479,6 +485,9 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(map_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -491,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -548,8 +557,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			map_number++;
 
 		fill_ident_line(tuple_store, tupdesc, map_number,
-						tok_line->line_num, identline,
-						tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num,
+						identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
new file mode 100644
index 0000000000..4d8d463d15
--- /dev/null
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open file "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open file \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	"could not open file \"$data_dir/recurse.conf\": maximum nesting depth exceeded");
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open file \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	"could not open file \"$data_dir/ident_recurse.conf\": maximum nesting depth exceeded");
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 624d0e5aae..90162b0fa9 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..2ae723de66 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -863,8 +914,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -875,6 +928,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7c716fe327..a21c3fee15 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1002,12 +1002,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1152,12 +1161,22 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this map
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this map in <filename>pg_ident.conf</filename>
+       Line number of this map in the corresponding
+       <literal>file_name</literal>
       </para></entry>
      </row>
 
-- 
2.38.1

#70Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#69)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Nov 07, 2022 at 03:07:15PM +0900, Michael Paquier wrote:

Attached is a set of three patches:
- 0001 changes tokenize_inc_file() to use AbsoluteConfigLocation().
AbsoluteConfigLocation() uses a static buffer and a MAXPGPATH, but
we'd rather change it to use a palloc()+strcpy() instead and remove
the static restriction? What do you think? The same applies for the
case where we use DataDir, actually, and it seems like there is no
point in this path-length restriction in this code path.
- 0002 invents the interface to open auth files and check for their
depths, simplifying the main patch a bit as there is no need to track
the depth level here and there anymore.
- 0003 is the rebased patch, simplified after the other changes. The
bulk of the patch is in its TAP test.

CF bot unhappy as I have messed up with rules.out. Rebased. I have
removed the restriction on MAXPGPATH in AbsoluteConfigLocation() in
0001, while on it. The absolute paths built on GUC or ident
inclusions are the same.
--
Michael

Attachments:

v17-0001-Expand-the-use-of-AbsoluteConfigLocation-in-hba..patchtext/x-diff; charset=us-asciiDownload
From f056ccbd604d63185763f774c5105f3208919306 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 8 Nov 2022 09:51:20 +0900
Subject: [PATCH v17 1/3] Expand the use of AbsoluteConfigLocation() in hba.c

The logic in charge of expanding an include file for database and user
names used the same code as AbsoluteConfigLocation() when building the
configuration file to include, so simplify this code.  While on it,
remove the restriction to MAXPGPATH, and switch to the same method as
what tokenize_inc_file() used.
---
 src/backend/libpq/hba.c            | 17 ++---------------
 src/backend/utils/misc/conffiles.c | 12 ++++++++----
 2 files changed, 10 insertions(+), 19 deletions(-)

diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index e9fc0af7c9..a9f87ab5bf 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -41,6 +41,7 @@
 #include "storage/fd.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/conffiles.h"
 #include "utils/guc.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -466,21 +467,7 @@ tokenize_inc_file(List *tokens,
 	ListCell   *inc_line;
 	MemoryContext linecxt;
 
-	if (is_absolute_path(inc_filename))
-	{
-		/* absolute path is taken as-is */
-		inc_fullname = pstrdup(inc_filename);
-	}
-	else
-	{
-		/* relative path is relative to dir of calling file */
-		inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
-									   strlen(inc_filename) + 1);
-		strcpy(inc_fullname, outer_filename);
-		get_parent_directory(inc_fullname);
-		join_path_components(inc_fullname, inc_fullname, inc_filename);
-		canonicalize_path(inc_fullname);
-	}
+	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
 
 	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
diff --git a/src/backend/utils/misc/conffiles.c b/src/backend/utils/misc/conffiles.c
index 4a99a1961e..35e2a3790b 100644
--- a/src/backend/utils/misc/conffiles.c
+++ b/src/backend/utils/misc/conffiles.c
@@ -35,15 +35,17 @@
 char *
 AbsoluteConfigLocation(const char *location, const char *calling_file)
 {
-	char		abs_path[MAXPGPATH];
-
 	if (is_absolute_path(location))
 		return pstrdup(location);
 	else
 	{
+		char	   *abs_path;
+
 		if (calling_file != NULL)
 		{
-			strlcpy(abs_path, calling_file, sizeof(abs_path));
+			abs_path = (char *) palloc0(strlen(calling_file) + 1 +
+										strlen(location) + 1);
+			strcpy(abs_path, calling_file);
 			get_parent_directory(abs_path);
 			join_path_components(abs_path, abs_path, location);
 			canonicalize_path(abs_path);
@@ -51,10 +53,12 @@ AbsoluteConfigLocation(const char *location, const char *calling_file)
 		else
 		{
 			Assert(DataDir);
+			abs_path = (char *) palloc0(strlen(DataDir) + 1 +
+										strlen(location) + 1);
 			join_path_components(abs_path, DataDir, location);
 			canonicalize_path(abs_path);
 		}
-		return pstrdup(abs_path);
+		return abs_path;
 	}
 }
 
-- 
2.38.1

v17-0002-Invent-open_auth_file-in-hba.c-to-refactor-auth-.patchtext/x-diff; charset=us-asciiDownload
From e201139be17e525c266240acf762c2b6a9fe6436 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 7 Nov 2022 13:35:44 +0900
Subject: [PATCH v17 2/3] Invent open_auth_file() in hba.c, to refactor auth
 file opening

This adds a check on the recursion depth when including auth files,
something that has never been done when processing '@' files for
database and user name lists in pg_hba.conf.
---
 src/include/libpq/hba.h          |   4 +-
 src/backend/libpq/hba.c          | 100 ++++++++++++++++++++++---------
 src/backend/utils/adt/hbafuncs.c |  18 ++----
 3 files changed, 79 insertions(+), 43 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 7ad227d34a..a84a5f0961 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -177,7 +177,9 @@ extern int	check_usermap(const char *usermap_name,
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
+extern FILE *open_auth_file(const char *filename, int elevel, int depth,
+							char **err_msg);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int elevel, int depth);
 
 #endif							/* HBA_H */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index a9f87ab5bf..d8c0b585e5 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -117,7 +117,8 @@ static const char *const UserAuthName[] =
 
 
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int elevel,
+							   int depth, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
@@ -414,7 +415,7 @@ regexec_auth_token(const char *match, AuthToken *token, size_t nmatch,
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int elevel, int depth, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -431,7 +432,7 @@ next_field_expand(const char *filename, char **lineptr,
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
 			tokens = tokenize_inc_file(tokens, filename, buf + 1,
-									   elevel, err_msg);
+									   elevel, depth + 1, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
 	} while (trailing_comma && (*err_msg == NULL));
@@ -459,6 +460,7 @@ tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
 				  int elevel,
+				  int depth,
 				  char **err_msg)
 {
 	char	   *inc_fullname;
@@ -468,24 +470,18 @@ tokenize_inc_file(List *tokens,
 	MemoryContext linecxt;
 
 	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, err_msg);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
 	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
+		/* error already logged */
 		pfree(inc_fullname);
 		return tokens;
 	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel,
+								 depth);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -521,6 +517,59 @@ tokenize_inc_file(List *tokens,
 	return tokens;
 }
 
+/*
+ * open_auth_file
+ *		Open the given file.
+ *
+ * filename: the absolute path to the target file
+ * elevel: message logging level
+ * depth: recursion level of the file opened.
+ * err_msg: details about the error.
+ *
+ * Return value is the opened file.  On error, returns NULL with details
+ * about the error stored in "err_msg".
+ */
+FILE *
+open_auth_file(const char *filename, int elevel, int depth,
+			   char **err_msg)
+{
+	FILE	*file;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open file \"%s\": maximum nesting depth exceeded",
+						filename)));
+		if (err_msg)
+			*err_msg = psprintf("could not open file \"%s\": maximum nesting depth exceeded",
+								filename);
+		return NULL;
+	}
+
+	file = AllocateFile(filename, "r");
+	if (file == NULL)
+	{
+		int			save_errno = errno;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open file \"%s\": %m",
+						filename)));
+		if (err_msg)
+			*err_msg = psprintf("could not open file \"%s\": %s",
+								filename, strerror(save_errno));
+		return NULL;
+	}
+
+	return file;
+}
+
 /*
  * tokenize_auth_file
  *		Tokenize the given file.
@@ -532,6 +581,7 @@ tokenize_inc_file(List *tokens,
  * file: the already-opened target file
  * tok_lines: receives output list
  * elevel: message logging level
+ * depth: level of recursion when tokenizing the target file
  *
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
@@ -542,7 +592,7 @@ tokenize_inc_file(List *tokens,
  */
 MemoryContext
 tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+				   int elevel, int depth)
 {
 	int			line_number = 1;
 	StringInfoData buf;
@@ -613,7 +663,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 			List	   *current_field;
 
 			current_field = next_field_expand(filename, &lineptr,
-											  elevel, &err_msg);
+											  elevel, depth, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
 				current_line = lappend(current_line, current_field);
@@ -2332,17 +2382,14 @@ load_hba(void)
 	MemoryContext oldcxt;
 	MemoryContext hbacxt;
 
-	file = AllocateFile(HbaFileName, "r");
+	file = open_auth_file(HbaFileName, LOG, 0, NULL);
 	if (file == NULL)
 	{
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
+		/* error already logged */
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2703,18 +2750,15 @@ load_ident(void)
 	MemoryContext ident_context;
 	IdentLine  *newline;
 
-	file = AllocateFile(IdentFileName, "r");
+	/* not FATAL ... we just won't do any special ident maps */
+	file = open_auth_file(IdentFileName, LOG, 0, NULL);
 	if (file == NULL)
 	{
-		/* not fatal ... we just won't do any special ident maps */
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open usermap file \"%s\": %m",
-						IdentFileName)));
+		/* error already logged */
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index e12ff8ca72..b662e7b55f 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -380,14 +380,9 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 * (Most other error conditions should result in a message in a view
 	 * entry.)
 	 */
-	file = AllocateFile(HbaFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
+	file = open_auth_file(HbaFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -529,14 +524,9 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 * (Most other error conditions should result in a message in a view
 	 * entry.)
 	 */
-	file = AllocateFile(IdentFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open usermap file \"%s\": %m",
-						IdentFileName)));
+	file = open_auth_file(IdentFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
-- 
2.38.1

v17-0003-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/x-diff; charset=us-asciiDownload
From 4dad3cf6aefb813bb3bd1f4b2fed3c801a608b95 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 8 Nov 2022 09:57:30 +0900
Subject: [PATCH v17 3/3] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/backend/libpq/hba.c                       | 233 ++++++-
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  39 +-
 .../authentication/t/004_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  23 +-
 9 files changed, 1020 insertions(+), 76 deletions(-)
 create mode 100644 src/test/authentication/t/004_file_inclusion.pl

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 20f5aa56ea..a1d9bef0e9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,16 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{map_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{map_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index d8c0b585e5..bd38ee0ba6 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -71,6 +71,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -116,6 +122,10 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel,
 							   int depth, char **err_msg);
@@ -125,6 +135,10 @@ static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 							   char **err_msg, int elevel);
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -572,11 +586,38 @@ open_auth_file(const char *filename, int elevel, int depth,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -586,30 +627,22 @@ open_auth_file(const char *filename, int elevel, int depth,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel, int depth)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel, int depth)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -670,30 +703,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedAuthLine *tok_line;
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->file_name = pstrdup(filename);
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetConfFilesInDir(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -2480,6 +2610,53 @@ load_hba(void)
 }
 
 
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, &err_msg);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_auth_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+		{
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							inc_fullname)));
+			return NULL;
+		}
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, elevel, depth);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
+
 /*
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index b662e7b55f..f9c99d41c6 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int map_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int map_number, char *filename, int lineno,
+							IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, char *filename,int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -203,6 +204,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(rule_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -346,7 +350,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -404,7 +408,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			rule_number++;
 
 		fill_hba_line(tuple_store, tupdesc, rule_number,
-					  tok_line->line_num, hbaline, tok_line->err_msg);
+					  tok_line->file_name, tok_line->line_num, hbaline,
+					  tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -441,7 +446,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -450,7 +455,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * map_number: unique identifier among all valid maps
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -459,7 +465,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int map_number, int lineno, IdentLine *ident,
+				int map_number, char *filename, int lineno, IdentLine *ident,
 				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -479,6 +485,9 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(map_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -491,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -548,8 +557,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			map_number++;
 
 		fill_ident_line(tuple_store, tupdesc, map_number,
-						tok_line->line_num, identline,
-						tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num,
+						identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
new file mode 100644
index 0000000000..4d8d463d15
--- /dev/null
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open file "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open file \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	"could not open file \"$data_dir/recurse.conf\": maximum nesting depth exceeded");
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open file \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	"could not open file \"$data_dir/ident_recurse.conf\": maximum nesting depth exceeded");
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 624d0e5aae..4c6c25dbb6 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.map_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(map_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(map_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..2ae723de66 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -863,8 +914,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -875,6 +928,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7c716fe327..a21c3fee15 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1002,12 +1002,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1152,12 +1161,22 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this map
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this map in <filename>pg_ident.conf</filename>
+       Line number of this map in the corresponding
+       <literal>file_name</literal>
       </para></entry>
      </row>
 
-- 
2.38.1

#71Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#70)
2 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Nov 08, 2022 at 10:04:16AM +0900, Michael Paquier wrote:

CF bot unhappy as I have messed up with rules.out. Rebased. I have
removed the restriction on MAXPGPATH in AbsoluteConfigLocation() in
0001, while on it. The absolute paths built on GUC or ident
inclusions are the same.

Rebased after 6bbd8b7g that is an equivalent of the previous 0001.
Julien, please note that this is waiting on author for now. What do
you think about the now-named v18-0001 and the addition of an
ErrorContextCallback to provide more information about the list of
included files on error?
--
Michael

Attachments:

v18-0001-Invent-open_auth_file-in-hba.c-to-refactor-auth-.patchtext/x-diff; charset=us-asciiDownload
From 50441282e6b298ee737e6e1a490d77baedfe5d69 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 7 Nov 2022 13:35:44 +0900
Subject: [PATCH v18 1/2] Invent open_auth_file() in hba.c, to refactor auth
 file opening

This adds a check on the recursion depth when including auth files,
something that has never been done when processing '@' files for
database and user name lists in pg_hba.conf.
---
 src/include/libpq/hba.h          |   4 +-
 src/backend/libpq/hba.c          | 100 ++++++++++++++++++++++---------
 src/backend/utils/adt/hbafuncs.c |  18 ++----
 3 files changed, 79 insertions(+), 43 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 7ad227d34a..a84a5f0961 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -177,7 +177,9 @@ extern int	check_usermap(const char *usermap_name,
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
+extern FILE *open_auth_file(const char *filename, int elevel, int depth,
+							char **err_msg);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int elevel, int depth);
 
 #endif							/* HBA_H */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index a9f87ab5bf..d8c0b585e5 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -117,7 +117,8 @@ static const char *const UserAuthName[] =
 
 
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int elevel,
+							   int depth, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
@@ -414,7 +415,7 @@ regexec_auth_token(const char *match, AuthToken *token, size_t nmatch,
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int elevel, int depth, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -431,7 +432,7 @@ next_field_expand(const char *filename, char **lineptr,
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
 			tokens = tokenize_inc_file(tokens, filename, buf + 1,
-									   elevel, err_msg);
+									   elevel, depth + 1, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
 	} while (trailing_comma && (*err_msg == NULL));
@@ -459,6 +460,7 @@ tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
 				  int elevel,
+				  int depth,
 				  char **err_msg)
 {
 	char	   *inc_fullname;
@@ -468,24 +470,18 @@ tokenize_inc_file(List *tokens,
 	MemoryContext linecxt;
 
 	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, err_msg);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
 	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
+		/* error already logged */
 		pfree(inc_fullname);
 		return tokens;
 	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel,
+								 depth);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -521,6 +517,59 @@ tokenize_inc_file(List *tokens,
 	return tokens;
 }
 
+/*
+ * open_auth_file
+ *		Open the given file.
+ *
+ * filename: the absolute path to the target file
+ * elevel: message logging level
+ * depth: recursion level of the file opened.
+ * err_msg: details about the error.
+ *
+ * Return value is the opened file.  On error, returns NULL with details
+ * about the error stored in "err_msg".
+ */
+FILE *
+open_auth_file(const char *filename, int elevel, int depth,
+			   char **err_msg)
+{
+	FILE	*file;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open file \"%s\": maximum nesting depth exceeded",
+						filename)));
+		if (err_msg)
+			*err_msg = psprintf("could not open file \"%s\": maximum nesting depth exceeded",
+								filename);
+		return NULL;
+	}
+
+	file = AllocateFile(filename, "r");
+	if (file == NULL)
+	{
+		int			save_errno = errno;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open file \"%s\": %m",
+						filename)));
+		if (err_msg)
+			*err_msg = psprintf("could not open file \"%s\": %s",
+								filename, strerror(save_errno));
+		return NULL;
+	}
+
+	return file;
+}
+
 /*
  * tokenize_auth_file
  *		Tokenize the given file.
@@ -532,6 +581,7 @@ tokenize_inc_file(List *tokens,
  * file: the already-opened target file
  * tok_lines: receives output list
  * elevel: message logging level
+ * depth: level of recursion when tokenizing the target file
  *
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
@@ -542,7 +592,7 @@ tokenize_inc_file(List *tokens,
  */
 MemoryContext
 tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+				   int elevel, int depth)
 {
 	int			line_number = 1;
 	StringInfoData buf;
@@ -613,7 +663,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 			List	   *current_field;
 
 			current_field = next_field_expand(filename, &lineptr,
-											  elevel, &err_msg);
+											  elevel, depth, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
 				current_line = lappend(current_line, current_field);
@@ -2332,17 +2382,14 @@ load_hba(void)
 	MemoryContext oldcxt;
 	MemoryContext hbacxt;
 
-	file = AllocateFile(HbaFileName, "r");
+	file = open_auth_file(HbaFileName, LOG, 0, NULL);
 	if (file == NULL)
 	{
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
+		/* error already logged */
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2703,18 +2750,15 @@ load_ident(void)
 	MemoryContext ident_context;
 	IdentLine  *newline;
 
-	file = AllocateFile(IdentFileName, "r");
+	/* not FATAL ... we just won't do any special ident maps */
+	file = open_auth_file(IdentFileName, LOG, 0, NULL);
 	if (file == NULL)
 	{
-		/* not fatal ... we just won't do any special ident maps */
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open usermap file \"%s\": %m",
-						IdentFileName)));
+		/* error already logged */
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index e12ff8ca72..b662e7b55f 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -380,14 +380,9 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 * (Most other error conditions should result in a message in a view
 	 * entry.)
 	 */
-	file = AllocateFile(HbaFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
+	file = open_auth_file(HbaFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -529,14 +524,9 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 * (Most other error conditions should result in a message in a view
 	 * entry.)
 	 */
-	file = AllocateFile(IdentFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open usermap file \"%s\": %m",
-						IdentFileName)));
+	file = open_auth_file(IdentFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
-- 
2.38.1

v18-0002-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/x-diff; charset=us-asciiDownload
From ab0d1d13edacfaa951b834ce71573671f05be41e Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 8 Nov 2022 09:57:30 +0900
Subject: [PATCH v18 2/2] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/backend/libpq/hba.c                       | 233 ++++++-
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  39 +-
 .../authentication/t/004_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  23 +-
 9 files changed, 1020 insertions(+), 76 deletions(-)
 create mode 100644 src/test/authentication/t/004_file_inclusion.pl

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 20f5aa56ea..a1d9bef0e9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,16 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{map_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{map_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index d8c0b585e5..bd38ee0ba6 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -71,6 +71,12 @@ typedef struct check_network_data
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -116,6 +122,10 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel,
 							   int depth, char **err_msg);
@@ -125,6 +135,10 @@ static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 							   char **err_msg, int elevel);
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -572,11 +586,38 @@ open_auth_file(const char *filename, int elevel, int depth,
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -586,30 +627,22 @@ open_auth_file(const char *filename, int elevel, int depth,
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel, int depth)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel, int depth)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -670,30 +703,127 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedAuthLine *tok_line;
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->file_name = pstrdup(filename);
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetConfFilesInDir(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -2480,6 +2610,53 @@ load_hba(void)
 }
 
 
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, &err_msg);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_auth_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+		{
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							inc_fullname)));
+			return NULL;
+		}
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, elevel, depth);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
+
 /*
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index b662e7b55f..f9c99d41c6 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int map_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int map_number, char *filename, int lineno,
+							IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, char *filename,int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -203,6 +204,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(rule_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -346,7 +350,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -404,7 +408,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			rule_number++;
 
 		fill_hba_line(tuple_store, tupdesc, rule_number,
-					  tok_line->line_num, hbaline, tok_line->err_msg);
+					  tok_line->file_name, tok_line->line_num, hbaline,
+					  tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -441,7 +446,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -450,7 +455,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * map_number: unique identifier among all valid maps
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -459,7 +465,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int map_number, int lineno, IdentLine *ident,
+				int map_number, char *filename, int lineno, IdentLine *ident,
 				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -479,6 +485,9 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(map_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -491,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -548,8 +557,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			map_number++;
 
 		fill_ident_line(tuple_store, tupdesc, map_number,
-						tok_line->line_num, identline,
-						tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num,
+						identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
new file mode 100644
index 0000000000..4d8d463d15
--- /dev/null
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open file "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open file \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	"could not open file \"$data_dir/recurse.conf\": maximum nesting depth exceeded");
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open file \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	"could not open file \"$data_dir/ident_recurse.conf\": maximum nesting depth exceeded");
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 624d0e5aae..4c6c25dbb6 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.map_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(map_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(map_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..2ae723de66 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -863,8 +914,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -875,6 +928,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7c716fe327..a21c3fee15 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1002,12 +1002,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1152,12 +1161,22 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this map
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this map in <filename>pg_ident.conf</filename>
+       Line number of this map in the corresponding
+       <literal>file_name</literal>
       </para></entry>
      </row>
 
-- 
2.38.1

#72Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#71)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Wed, Nov 09, 2022 at 09:51:17AM +0900, Michael Paquier wrote:

On Tue, Nov 08, 2022 at 10:04:16AM +0900, Michael Paquier wrote:

CF bot unhappy as I have messed up with rules.out. Rebased. I have
removed the restriction on MAXPGPATH in AbsoluteConfigLocation() in
0001, while on it. The absolute paths built on GUC or ident
inclusions are the same.

Rebased after 6bbd8b7g that is an equivalent of the previous 0001.

Thanks a lot!

Julien, please note that this is waiting on author for now. What do
you think about the now-named v18-0001 and the addition of an
ErrorContextCallback to provide more information about the list of
included files on error?

Yes, I'm unfortunately fully aware that it's waiting on me. I've been a bit
busy this week with $work but I will try to go back to it as soon as I can,
hopefully this week!

#73Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#72)
3 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Nov 09, 2022 at 12:09:01PM +0800, Julien Rouhaud wrote:

On Wed, Nov 09, 2022 at 09:51:17AM +0900, Michael Paquier wrote:

Julien, please note that this is waiting on author for now. What do
you think about the now-named v18-0001 and the addition of an
ErrorContextCallback to provide more information about the list of
included files on error?

Yes, I'm unfortunately fully aware that it's waiting on me. I've been a bit
busy this week with $work but I will try to go back to it as soon as I can,
hopefully this week!

FWIW, I have been playing with the addition of a ErrorContextCallback
in tokenize_auth_file(), and this addition leads to a really nice
result. With this method, it is possible to know the full chain of
events leading to a failure when tokenizing included files, which is
not available now in the logs when reloading the server.

We could extend it to have more verbose information by passing more
arguments to tokenize_auth_file(), still I'd like to think that just
knowing the line number and the full path to the file is more than
enough once you know the full chain of events. 0001 and 0002 ought to
be merged together, but I am keeping these separate to show how simple
the addition of the ErrorContextCallback is.
--
Michael

Attachments:

v19-0001-Invent-open_auth_file-in-hba.c-to-refactor-auth-.patchtext/x-diff; charset=us-asciiDownload
From 41ae50e3b5b8e1d9013f337e4574702830dca95a Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 7 Nov 2022 13:35:44 +0900
Subject: [PATCH v19 1/3] Invent open_auth_file() in hba.c, to refactor auth
 file opening

This adds a check on the recursion depth when including auth files,
something that has never been done when processing '@' files for
database and user name lists in pg_hba.conf.
---
 src/include/libpq/hba.h          |   4 +-
 src/backend/libpq/hba.c          | 100 ++++++++++++++++++++++---------
 src/backend/utils/adt/hbafuncs.c |  18 ++----
 3 files changed, 79 insertions(+), 43 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index 7ad227d34a..a84a5f0961 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -177,7 +177,9 @@ extern int	check_usermap(const char *usermap_name,
 extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel);
 extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
+extern FILE *open_auth_file(const char *filename, int elevel, int depth,
+							char **err_msg);
 extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel);
+										List **tok_lines, int elevel, int depth);
 
 #endif							/* HBA_H */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index a9f87ab5bf..d8c0b585e5 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -117,7 +117,8 @@ static const char *const UserAuthName[] =
 
 
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel, char **err_msg);
+							   const char *inc_filename, int elevel,
+							   int depth, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
@@ -414,7 +415,7 @@ regexec_auth_token(const char *match, AuthToken *token, size_t nmatch,
  */
 static List *
 next_field_expand(const char *filename, char **lineptr,
-				  int elevel, char **err_msg)
+				  int elevel, int depth, char **err_msg)
 {
 	char		buf[MAX_TOKEN];
 	bool		trailing_comma;
@@ -431,7 +432,7 @@ next_field_expand(const char *filename, char **lineptr,
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
 			tokens = tokenize_inc_file(tokens, filename, buf + 1,
-									   elevel, err_msg);
+									   elevel, depth + 1, err_msg);
 		else
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
 	} while (trailing_comma && (*err_msg == NULL));
@@ -459,6 +460,7 @@ tokenize_inc_file(List *tokens,
 				  const char *outer_filename,
 				  const char *inc_filename,
 				  int elevel,
+				  int depth,
 				  char **err_msg)
 {
 	char	   *inc_fullname;
@@ -468,24 +470,18 @@ tokenize_inc_file(List *tokens,
 	MemoryContext linecxt;
 
 	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, err_msg);
 
-	inc_file = AllocateFile(inc_fullname, "r");
 	if (inc_file == NULL)
 	{
-		int			save_errno = errno;
-
-		ereport(elevel,
-				(errcode_for_file_access(),
-				 errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
-						inc_filename, inc_fullname)));
-		*err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s",
-							inc_filename, inc_fullname, strerror(save_errno));
+		/* error already logged */
 		pfree(inc_fullname);
 		return tokens;
 	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel);
+	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel,
+								 depth);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
@@ -521,6 +517,59 @@ tokenize_inc_file(List *tokens,
 	return tokens;
 }
 
+/*
+ * open_auth_file
+ *		Open the given file.
+ *
+ * filename: the absolute path to the target file
+ * elevel: message logging level
+ * depth: recursion level of the file opened.
+ * err_msg: details about the error.
+ *
+ * Return value is the opened file.  On error, returns NULL with details
+ * about the error stored in "err_msg".
+ */
+FILE *
+open_auth_file(const char *filename, int elevel, int depth,
+			   char **err_msg)
+{
+	FILE	*file;
+
+	/*
+	 * Reject too-deep include nesting depth.  This is just a safety check to
+	 * avoid dumping core due to stack overflow if an include file loops back
+	 * to itself.  The maximum nesting depth is pretty arbitrary.
+	 */
+	if (depth > 10)
+	{
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open file \"%s\": maximum nesting depth exceeded",
+						filename)));
+		if (err_msg)
+			*err_msg = psprintf("could not open file \"%s\": maximum nesting depth exceeded",
+								filename);
+		return NULL;
+	}
+
+	file = AllocateFile(filename, "r");
+	if (file == NULL)
+	{
+		int			save_errno = errno;
+
+		ereport(elevel,
+				(errcode_for_file_access(),
+				 errmsg("could not open file \"%s\": %m",
+						filename)));
+		if (err_msg)
+			*err_msg = psprintf("could not open file \"%s\": %s",
+								filename, strerror(save_errno));
+		return NULL;
+	}
+
+	return file;
+}
+
 /*
  * tokenize_auth_file
  *		Tokenize the given file.
@@ -532,6 +581,7 @@ tokenize_inc_file(List *tokens,
  * file: the already-opened target file
  * tok_lines: receives output list
  * elevel: message logging level
+ * depth: level of recursion when tokenizing the target file
  *
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
@@ -542,7 +592,7 @@ tokenize_inc_file(List *tokens,
  */
 MemoryContext
 tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel)
+				   int elevel, int depth)
 {
 	int			line_number = 1;
 	StringInfoData buf;
@@ -613,7 +663,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 			List	   *current_field;
 
 			current_field = next_field_expand(filename, &lineptr,
-											  elevel, &err_msg);
+											  elevel, depth, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
 				current_line = lappend(current_line, current_field);
@@ -2332,17 +2382,14 @@ load_hba(void)
 	MemoryContext oldcxt;
 	MemoryContext hbacxt;
 
-	file = AllocateFile(HbaFileName, "r");
+	file = open_auth_file(HbaFileName, LOG, 0, NULL);
 	if (file == NULL)
 	{
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
+		/* error already logged */
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2703,18 +2750,15 @@ load_ident(void)
 	MemoryContext ident_context;
 	IdentLine  *newline;
 
-	file = AllocateFile(IdentFileName, "r");
+	/* not FATAL ... we just won't do any special ident maps */
+	file = open_auth_file(IdentFileName, LOG, 0, NULL);
 	if (file == NULL)
 	{
-		/* not fatal ... we just won't do any special ident maps */
-		ereport(LOG,
-				(errcode_for_file_access(),
-				 errmsg("could not open usermap file \"%s\": %m",
-						IdentFileName)));
+		/* error already logged */
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index e12ff8ca72..b662e7b55f 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -380,14 +380,9 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 * (Most other error conditions should result in a message in a view
 	 * entry.)
 	 */
-	file = AllocateFile(HbaFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open configuration file \"%s\": %m",
-						HbaFileName)));
+	file = open_auth_file(HbaFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3);
+	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -529,14 +524,9 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 * (Most other error conditions should result in a message in a view
 	 * entry.)
 	 */
-	file = AllocateFile(IdentFileName, "r");
-	if (file == NULL)
-		ereport(ERROR,
-				(errcode_for_file_access(),
-				 errmsg("could not open usermap file \"%s\": %m",
-						IdentFileName)));
+	file = open_auth_file(IdentFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3);
+	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
-- 
2.38.1

v19-0002-Add-error-context-callback-when-tokenizing-HBA-f.patchtext/x-diff; charset=us-asciiDownload
From bcbe01850df5a624510c7cf0d0ee1836ff6f57bd Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 10 Nov 2022 10:14:30 +0900
Subject: [PATCH v19 2/3] Add error context callback when tokenizing HBA files

---
 src/backend/libpq/hba.c | 31 +++++++++++++++++++++++++++++++
 1 file changed, 31 insertions(+)

diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index d8c0b585e5..294fc1383c 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -66,6 +66,11 @@ typedef struct check_network_data
 	bool		result;			/* set to true if match */
 } check_network_data;
 
+typedef struct
+{
+	const char *filename;
+	int			linenum;
+} tokenize_error_callback_arg;
 
 #define token_has_regexp(t)	(t->regex != NULL)
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
@@ -125,6 +130,7 @@ static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 							   char **err_msg, int elevel);
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
+static void tokenize_error_callback(void *arg);
 
 
 /*
@@ -570,6 +576,18 @@ open_auth_file(const char *filename, int elevel, int depth,
 	return file;
 }
 
+/*
+ * error context callback for tokenize_auth_file()
+ */
+static void
+tokenize_error_callback(void *arg)
+{
+	tokenize_error_callback_arg *callback_arg = (tokenize_error_callback_arg *) arg;
+
+	errcontext("line %d of configuration file \"%s\"",
+			   callback_arg->linenum, callback_arg->filename);
+}
+
 /*
  * tokenize_auth_file
  *		Tokenize the given file.
@@ -598,6 +616,16 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 	StringInfoData buf;
 	MemoryContext linecxt;
 	MemoryContext oldcxt;
+	ErrorContextCallback tokenerrcontext;
+	tokenize_error_callback_arg callback_arg;
+
+	callback_arg.filename = filename;
+	callback_arg.linenum = line_number;
+
+	tokenerrcontext.callback = tokenize_error_callback;
+	tokenerrcontext.arg = (void *) &callback_arg;
+	tokenerrcontext.previous = error_context_stack;
+	error_context_stack = &tokenerrcontext;
 
 	linecxt = AllocSetContextCreate(CurrentMemoryContext,
 									"tokenize_auth_file",
@@ -686,10 +714,13 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		line_number += continuations + 1;
+		callback_arg.linenum = line_number;
 	}
 
 	MemoryContextSwitchTo(oldcxt);
 
+	error_context_stack = tokenerrcontext.previous;
+
 	return linecxt;
 }
 
-- 
2.38.1

v19-0003-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/x-diff; charset=us-asciiDownload
From 6e2da7185927a8988a0f302799fe91f0771af24b Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 8 Nov 2022 09:57:30 +0900
Subject: [PATCH v19 3/3] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

This fixes a possible crash if a secondary file tries to include itself as
there's now a nesting depth check in the inclusion code path, same as the
postgresql.conf.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/backend/libpq/hba.c                       | 233 ++++++-
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  39 +-
 .../authentication/t/004_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  23 +-
 9 files changed, 1020 insertions(+), 76 deletions(-)
 create mode 100644 src/test/authentication/t/004_file_inclusion.pl

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 20f5aa56ea..a1d9bef0e9 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6135,16 +6135,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{map_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{map_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 294fc1383c..c4473ab1ea 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -76,6 +76,12 @@ typedef struct
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -121,6 +127,10 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel,
 							   int depth, char **err_msg);
@@ -131,6 +141,10 @@ static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
 static void tokenize_error_callback(void *arg);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -590,11 +604,38 @@ tokenize_error_callback(void *arg)
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -604,17 +645,13 @@ tokenize_error_callback(void *arg)
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel, int depth)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel, int depth)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 	ErrorContextCallback tokenerrcontext;
 	tokenize_error_callback_arg callback_arg;
@@ -627,17 +664,13 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 	tokenerrcontext.previous = error_context_stack;
 	error_context_stack = &tokenerrcontext;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -698,21 +731,121 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedAuthLine *tok_line;
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->file_name = pstrdup(filename);
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetConfFilesInDir(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 		callback_arg.linenum = line_number;
 	}
@@ -720,11 +853,8 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 	MemoryContextSwitchTo(oldcxt);
 
 	error_context_stack = tokenerrcontext.previous;
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -2511,6 +2641,53 @@ load_hba(void)
 }
 
 
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, &err_msg);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_auth_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+		{
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							inc_fullname)));
+			return NULL;
+		}
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, elevel, depth);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
+
 /*
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index b662e7b55f..f9c99d41c6 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int map_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int map_number, char *filename, int lineno,
+							IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, char *filename,int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -203,6 +204,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(rule_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -346,7 +350,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -404,7 +408,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			rule_number++;
 
 		fill_hba_line(tuple_store, tupdesc, rule_number,
-					  tok_line->line_num, hbaline, tok_line->err_msg);
+					  tok_line->file_name, tok_line->line_num, hbaline,
+					  tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -441,7 +446,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -450,7 +455,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * map_number: unique identifier among all valid maps
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -459,7 +465,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int map_number, int lineno, IdentLine *ident,
+				int map_number, char *filename, int lineno, IdentLine *ident,
 				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -479,6 +485,9 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(map_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -491,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -548,8 +557,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			map_number++;
 
 		fill_ident_line(tuple_store, tupdesc, map_number,
-						tok_line->line_num, identline,
-						tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num,
+						identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
new file mode 100644
index 0000000000..4d8d463d15
--- /dev/null
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open file "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open file \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	"could not open file \"$data_dir/recurse.conf\": maximum nesting depth exceeded");
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open file \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	"could not open file \"$data_dir/ident_recurse.conf\": maximum nesting depth exceeded");
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 624d0e5aae..4c6c25dbb6 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.map_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(map_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(map_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..2ae723de66 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -863,8 +914,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -875,6 +928,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7c716fe327..a21c3fee15 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1002,12 +1002,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1152,12 +1161,22 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this map
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this map in <filename>pg_ident.conf</filename>
+       Line number of this map in the corresponding
+       <literal>file_name</literal>
       </para></entry>
      </row>
 
-- 
2.38.1

#74Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#73)
Re: Allow file inclusion in pg_hba and pg_ident files

On Thu, Nov 10, 2022 at 10:29:40AM +0900, Michael Paquier wrote:

FWIW, I have been playing with the addition of a ErrorContextCallback
in tokenize_auth_file(), and this addition leads to a really nice
result. With this method, it is possible to know the full chain of
events leading to a failure when tokenizing included files, which is
not available now in the logs when reloading the server.

We could extend it to have more verbose information by passing more
arguments to tokenize_auth_file(), still I'd like to think that just
knowing the line number and the full path to the file is more than
enough once you know the full chain of events. 0001 and 0002 ought to
be merged together, but I am keeping these separate to show how simple
the addition of the ErrorContextCallback is.

It's looks good to me. I agree that file name and line number should be enough
to diagnose any unexpected error.

#75Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#74)
Re: Allow file inclusion in pg_hba and pg_ident files

On Sat, Nov 12, 2022 at 04:13:53PM +0800, Julien Rouhaud wrote:

It's looks good to me. I agree that file name and line number should be enough
to diagnose any unexpected error.

Thanks for checking. I have looked at 0001 and 0002 again with a
fresh mind, and applied both of them this morning.

This makes the remaining bits of the patch much easier to follow in
hba.c. Here are more comments after a closer review of the whole for
the C logic.

-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-                  int elevel, int depth)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,

I really tend to prefer having one routine rather than two for the
tokenization entry point. Switching to the line context after setting
up the callback is better, and tokenize_file_with_context() does so.
Anyway, what do you think about having one API that gains a
"MemoryContext *" argument, as of the following:
void tokenize_auth_file(const char *filename, FILE *file,
List **tok_lines,
int depth, int elevel, MemoryContext *linectx)

If the caller passes NULL for *linectx as the initial line context,
just create it as we do now. If *linectx is not NULL, just reuse it.
That may be cleaner than returning the created MemoryContext as
returned result from tokenize_auth_file().

+    /* Cumulate errors if any. */
+    if (err_msg)
+    {
+        if (err_buf.len > 0)
+            appendStringInfoChar(&err_buf, '\n');
+        appendStringInfoString(&err_buf, err_msg);
+    }

This aggregates all the error messages for all the files included in a
given repository. As the patch stands, it seems to me that we would
get errors related to an include_dir clause for two cases:
- The specified path does not exist, in which case we have only one
err_msg to consume and report back.
- Multiple failures in opening and/or reading included files.
In the second case, aggregating the reports would provide a full set
of information, but that's not something a user would be able to act
on directly as this is system-related. Or there is a case to know a
full list of files in the case of multiple files that cannot be read
because of permission issues? We may be fine with just the first
system error here. Note that in the case where files can be read and
opened, these would have their own TokenizedAuthLines for each line
parsed, meaning one line in the SQL views once translated to an
HbaLine or an IdentLine.

This line of thoughts brings an interesting point, actually: there is
an inconsistency between "include_if_exists" and "include" compared to
the GUC processing. As of the patch, if we use "include" on a file
that does not exist, the tokenization logic would jump over it and
continue processing the follow-up entries anyway. This is a different
policy than the GUCs, where we would immediately stop looking at
parameters after an "include" if it fails because its file does not
exist, working as a immediate stop in the processing. The difference
that the patch brings between "include_if_exists" and "include" is
that we report an error in one case but not the other, still skip the
files in both cases and move on with the rest. Hence my question,
shouldn't we do like the GUC processing for the hba and ident files,
aka stop immediately when we fail to find a file on an "include"
clause? This would be equivalent to doing a "break" in
tokenize_file_with_context() after failing an include file.
--
Michael

#76Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#75)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Mon, Nov 14, 2022 at 02:40:37PM +0900, Michael Paquier wrote:

On Sat, Nov 12, 2022 at 04:13:53PM +0800, Julien Rouhaud wrote:

It's looks good to me. I agree that file name and line number should be enough
to diagnose any unexpected error.

Thanks for checking. I have looked at 0001 and 0002 again with a
fresh mind, and applied both of them this morning.

Thanks a lot!

This makes the remaining bits of the patch much easier to follow in
hba.c. Here are more comments after a closer review of the whole for
the C logic.

Agreed.

-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-                  int elevel, int depth)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,

I really tend to prefer having one routine rather than two for the
tokenization entry point. Switching to the line context after setting
up the callback is better, and tokenize_file_with_context() does so.
Anyway, what do you think about having one API that gains a
"MemoryContext *" argument, as of the following:
void tokenize_auth_file(const char *filename, FILE *file,
List **tok_lines,
int depth, int elevel, MemoryContext *linectx)

If the caller passes NULL for *linectx as the initial line context,
just create it as we do now. If *linectx is not NULL, just reuse it.
That may be cleaner than returning the created MemoryContext as
returned result from tokenize_auth_file().

I originally used two functions as it's a common pattern (e.g. ReadBuffer /
ReadBufferExtended or all the *_internal versions of functions) and to avoid
unnecessary branch, but I agree that here having an extra branch is unlikely to
make any measurable difference. It would only matter on -DEXEC_BACKEND /
win32, and in that case the extra overhead (over an already expensive new
backend mechanism) is way more than that, so +1 to keep things simple here.

+    /* Cumulate errors if any. */
+    if (err_msg)
+    {
+        if (err_buf.len > 0)
+            appendStringInfoChar(&err_buf, '\n');
+        appendStringInfoString(&err_buf, err_msg);
+    }

This aggregates all the error messages for all the files included in a
given repository. As the patch stands, it seems to me that we would
get errors related to an include_dir clause for two cases:
- The specified path does not exist, in which case we have only one
err_msg to consume and report back.
- Multiple failures in opening and/or reading included files.
In the second case, aggregating the reports would provide a full set
of information, but that's not something a user would be able to act
on directly as this is system-related. Or there is a case to know a
full list of files in the case of multiple files that cannot be read
because of permission issues? We may be fine with just the first
system error here. Note that in the case where files can be read and
opened, these would have their own TokenizedAuthLines for each line
parsed, meaning one line in the SQL views once translated to an
HbaLine or an IdentLine.

If you have an include_dir directive and multiple files have wrong permission
(or maybe broken symlink or something like that), you will get multiple errors
when trying to process that single directive. I think it's friendlier to
report as much detail as we can, so users can make sure they fix everything
rather than iterating over the first error. That's especially helpful if the
fix is done in some external tooling (puppet or whatever) rather than directly
on the server.

This line of thoughts brings an interesting point, actually: there is
an inconsistency between "include_if_exists" and "include" compared to
the GUC processing. As of the patch, if we use "include" on a file
that does not exist, the tokenization logic would jump over it and
continue processing the follow-up entries anyway. This is a different
policy than the GUCs, where we would immediately stop looking at
parameters after an "include" if it fails because its file does not
exist, working as a immediate stop in the processing. The difference
that the patch brings between "include_if_exists" and "include" is
that we report an error in one case but not the other, still skip the
files in both cases and move on with the rest. Hence my question,
shouldn't we do like the GUC processing for the hba and ident files,
aka stop immediately when we fail to find a file on an "include"
clause? This would be equivalent to doing a "break" in
tokenize_file_with_context() after failing an include file.

I think that the problem is that we have the same interface for processing the
files on a startup/reload and for filling the views, so if we want the views to
be helpful and report all errors we have to also allow a bogus "include" to
continue in the reload case too. The same problem doesn't exists for GUCs, so
a slightly different behavior there might be acceptable.

#77Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#76)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Nov 14, 2022 at 03:47:27PM +0800, Julien Rouhaud wrote:

On Mon, Nov 14, 2022 at 02:40:37PM +0900, Michael Paquier wrote:

If the caller passes NULL for *linectx as the initial line context,
just create it as we do now. If *linectx is not NULL, just reuse it.
That may be cleaner than returning the created MemoryContext as
returned result from tokenize_auth_file().

I originally used two functions as it's a common pattern (e.g. ReadBuffer /
ReadBufferExtended or all the *_internal versions of functions) and to avoid
unnecessary branch, but I agree that here having an extra branch is unlikely to
make any measurable difference. It would only matter on -DEXEC_BACKEND /
win32, and in that case the extra overhead (over an already expensive new
backend mechanism) is way more than that, so +1 to keep things simple here.

Oh, Okay. So that was your intention here.

This aggregates all the error messages for all the files included in a
given repository. As the patch stands, it seems to me that we would
get errors related to an include_dir clause for two cases:
- The specified path does not exist, in which case we have only one
err_msg to consume and report back.
- Multiple failures in opening and/or reading included files.
In the second case, aggregating the reports would provide a full set
of information, but that's not something a user would be able to act
on directly as this is system-related. Or there is a case to know a
full list of files in the case of multiple files that cannot be read
because of permission issues? We may be fine with just the first
system error here. Note that in the case where files can be read and
opened, these would have their own TokenizedAuthLines for each line
parsed, meaning one line in the SQL views once translated to an
HbaLine or an IdentLine.

If you have an include_dir directive and multiple files have wrong permission
(or maybe broken symlink or something like that), you will get multiple errors
when trying to process that single directive. I think it's friendlier to
report as much detail as we can, so users can make sure they fix everything
rather than iterating over the first error. That's especially helpful if the
fix is done in some external tooling (puppet or whatever) rather than directly
on the server.

Hmm, Okay. Well, this would include only errors on I/O or permission
problems for existing files.. I have not seen deployments that have
dozens of sub-files for GUCs with an included dir, but perhaps I lack
of experience on user histories.

I think that the problem is that we have the same interface for processing the
files on a startup/reload and for filling the views, so if we want the views to
be helpful and report all errors we have to also allow a bogus "include" to
continue in the reload case too. The same problem doesn't exists for GUCs, so
a slightly different behavior there might be acceptable.

Well, there is as well the point that we don't have yet a view for
GUCs that does the equivalent of HBA and ident, but that does not
mean, it seems to me, that there should be an inconsistency in the way
we process those commands because one has implemented a feature but
not the other. On the contrary, I'd rather try to make them
consistent. As things are in the patch, the only difference between
"include_if_exists" and "include" is that the latter would report some
information if the file goes missing, the former generates a LOG entry
about the file being skipped without something in the system view.
Now, wouldn't it be useful for the end-user to report that a file is
skipped as an effect of "include_if_exists" in the system views? If
not, then what's the point of having this clause to begin with? My
opinion is that both clauses are useful, still on the ground of
consistency both clauses should work the same as for GUCs. Both
should report something in err_msg even if that's just about a file
being skipped, though I agree that this could be considered confusing
as well for an if_exists clause (does not look like a big deal to me
based on the debuggability gain).
--
Michael

#78Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#77)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Nov 15, 2022 at 08:46:55AM +0900, Michael Paquier wrote:

On Mon, Nov 14, 2022 at 03:47:27PM +0800, Julien Rouhaud wrote:

If you have an include_dir directive and multiple files have wrong permission
(or maybe broken symlink or something like that), you will get multiple errors
when trying to process that single directive. I think it's friendlier to
report as much detail as we can, so users can make sure they fix everything
rather than iterating over the first error. That's especially helpful if the
fix is done in some external tooling (puppet or whatever) rather than directly
on the server.

Hmm, Okay. Well, this would include only errors on I/O or permission
problems for existing files.. I have not seen deployments that have
dozens of sub-files for GUCs with an included dir, but perhaps I lack
of experience on user histories.

While being the same inclusion infrastructure, it's likely that people will
have different usage. I'm assuming that for GUCs the main usage is to have
your automation tool put one of your template conf for instance (small_vm.conf,
big_vm.conf or something like that) in an included directory, so you don't
really need a lot of them. You can also rely on ALTER SYSTEM to avoid manually
handling configuration files entirely.

For authentication there are probably very different pattern. The use case I
had when writing this patch is some complex application that relies on many
services, each service having dedicated role and authentication, and services
can be enabled or disabled dynamically pretty much anytime. I had to write
code to merge possibly new entries with existing pg_hba/pg_ident configuration
files. With this feature it would be much easier (and robust) to simply have a
main pg_hba/pg_ident that includes some directory, and have the service
enable/disable simply creates/removes a dedicated file for each service, and we
then would usually have at least a dozen files there.

I'm assuming people doing multi-tenant can also have similar usage.

I think that the problem is that we have the same interface for processing the
files on a startup/reload and for filling the views, so if we want the views to
be helpful and report all errors we have to also allow a bogus "include" to
continue in the reload case too. The same problem doesn't exists for GUCs, so
a slightly different behavior there might be acceptable.

Well, there is as well the point that we don't have yet a view for
GUCs that does the equivalent of HBA and ident,

Yes that's what I meant by "the same problem doesn't exists for GUCs".

but that does not
mean, it seems to me, that there should be an inconsistency in the way
we process those commands because one has implemented a feature but
not the other. On the contrary, I'd rather try to make them
consistent.

You mean stopping at the first error, even if it's only for the view reporting?
That will make the reload consistent, but the view will be a bit useless then.

As things are in the patch, the only difference between
"include_if_exists" and "include" is that the latter would report some
information if the file goes missing, the former generates a LOG entry
about the file being skipped without something in the system view.
Now, wouldn't it be useful for the end-user to report that a file is
skipped as an effect of "include_if_exists" in the system views? If
not, then what's the point of having this clause to begin with?

I don't really get the argument "the proposed monitoring view doesn't give this
information, so the feature isn't needed". Also, unless I'm missing something
the other difference between "include" and "include_if_exists" is that the
first one will refuse to reload the conf (or start the server) if the file is
missing, while the other one will?

My
opinion is that both clauses are useful, still on the ground of
consistency both clauses should work the same as for GUCs. Both
should report something in err_msg even if that's just about a file
being skipped, though I agree that this could be considered confusing
as well for an if_exists clause (does not look like a big deal to me
based on the debuggability gain).

It would be nice to have the information that an "include_if_exists" file
didn't exist, but having a log-level message in the "error" column is a clear
POLA violation. People will probably just do something like

SELECT file_name, line_number, error FROM pg_hba_file_rules WHERE error IS NOT
NULL;

and report an error if any row is found. Having to parse the error field to
know if that's really an error or not is going to be a huge foot-gun. Maybe we
could indeed report the problem in err_msg but for include_if_exists display it
in some other column of the view?

#79Ian Lawrence Barwick
barwick@gmail.com
In reply to: Michael Paquier (#75)
Re: Allow file inclusion in pg_hba and pg_ident files

2022年11月14日(月) 14:41 Michael Paquier <michael@paquier.xyz>:

On Sat, Nov 12, 2022 at 04:13:53PM +0800, Julien Rouhaud wrote:

It's looks good to me. I agree that file name and line number should be enough
to diagnose any unexpected error.

Thanks for checking. I have looked at 0001 and 0002 again with a
fresh mind, and applied both of them this morning.

Hi

Just a quick note to mention that 0003 adds a new test ("004_file_inclusion.pl")
but doesn't seem to include it in the "meson.build" file.

Regards

Ian Barwick

#80Julien Rouhaud
rjuju123@gmail.com
In reply to: Ian Lawrence Barwick (#79)
Re: Allow file inclusion in pg_hba and pg_ident files

Le mer. 16 nov. 2022 à 13:01, Ian Lawrence Barwick <barwick@gmail.com> a
écrit :

2022年11月14日(月) 14:41 Michael Paquier <michael@paquier.xyz>:

On Sat, Nov 12, 2022 at 04:13:53PM +0800, Julien Rouhaud wrote:

It's looks good to me. I agree that file name and line number should

be enough

to diagnose any unexpected error.

Thanks for checking. I have looked at 0001 and 0002 again with a
fresh mind, and applied both of them this morning.

Hi

Just a quick note to mention that 0003 adds a new test ("
004_file_inclusion.pl")
but doesn't seem to include it in the "meson.build" file.

ah thanks! Michael also mentioned that previously but as we were focusing
on preliminary refactoring patches I didn't check if if was fixed in the
patches sent recently, but I will definitely take care of it for the next
round

Show quoted text
#81Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#78)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Nov 16, 2022 at 10:53:02AM +0800, Julien Rouhaud wrote:

While being the same inclusion infrastructure, it's likely that people will
have different usage. I'm assuming that for GUCs the main usage is to have
your automation tool put one of your template conf for instance (small_vm.conf,
big_vm.conf or something like that) in an included directory, so you don't
really need a lot of them. You can also rely on ALTER SYSTEM to avoid manually
handling configuration files entirely.

For authentication there are probably very different pattern. The use case I
had when writing this patch is some complex application that relies on many
services, each service having dedicated role and authentication, and services
can be enabled or disabled dynamically pretty much anytime. I had to write
code to merge possibly new entries with existing pg_hba/pg_ident configuration
files. With this feature it would be much easier (and robust) to simply have a
main pg_hba/pg_ident that includes some directory, and have the service
enable/disable simply creates/removes a dedicated file for each service, and we
then would usually have at least a dozen files there.

I'm assuming people doing multi-tenant can also have similar usage.

So you main application for HBA/ident is include_dir/.. For GUCs, we
would just skip the directory if it has no files, but the directory
has to exist. Your patch is behaving the same.

but that does not
mean, it seems to me, that there should be an inconsistency in the way
we process those commands because one has implemented a feature but
not the other. On the contrary, I'd rather try to make them
consistent.

You mean stopping at the first error, even if it's only for the view reporting?
That will make the reload consistent, but the view will be a bit useless then.

Yep, I meant to stop the generation of the TokenizedAuthLines at the
first inclusion error by letting tokenize_auth_file() return a status
to be able to stop the recursions. But after some second-thoughts
pondering about this, I see why I am wrong and why you are right. As
you say, stopping the generation of the TokenizedAuthLines would just
limit the amount of data reported at once in the view, the golden rule
for HBA/ident being that we would reload nothing as long as there is
at least one error reported when parsing things. So giving more
control to tokenize_auth_file() to stop a recursion, say by making it
return a boolean status, makes little sense.

It would be nice to have the information that an "include_if_exists" file
didn't exist, but having a log-level message in the "error" column is a clear
POLA violation. People will probably just do something like

SELECT file_name, line_number, error FROM pg_hba_file_rules WHERE error IS NOT
NULL;

and report an error if any row is found. Having to parse the error field to
know if that's really an error or not is going to be a huge foot-gun. Maybe we
could indeed report the problem in err_msg but for include_if_exists display it
in some other column of the view?

Hmm. One possibility would be to add a direct mention to the
"include", "include_dir" and "include_if_exists" clauses through
pg_hba_file_rules.type? I don't see how to do that without making the
patch much more invasive as per the existing separation between
tokenization and Hba/IdentLine filling, though, and perhaps the error
provided with the full path to the file would provide enough context
for one to know if the failure is happening on an included file for
database/user lists or a full file from an "include" clause. It also
means that include_if_exists remains transparent all the time.
Simpler may be for the best here, at the end.

By the way, I am wondering whether process_included_authfile() is
the most intuitive interface here. The only thing that prevents a
common routine to process the include commands is the failure on
GetConfFilesInDir(), where we need to build a TokenizedAuthLine when
the others have already done so tokenize_file_with_context(). Could
it be cleaner to have a small routine like makeTokenizedAuthLine()
that gets reused when we fail scanning a directory to build a
TokenizedAuthLine, in combination with a small-ish routine working on
a directory like ParseConfigDirectory() but for HBA/ident? Or we
could just drop process_included_authfile() entirely? On failure,
this would make the code do a next_line all the time for all the
include clauses.
--
Michael

#82Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#81)
2 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Thu, Nov 17, 2022 at 11:33:05AM +0900, Michael Paquier wrote:

By the way, I am wondering whether process_included_authfile() is
the most intuitive interface here. The only thing that prevents a
common routine to process the include commands is the failure on
GetConfFilesInDir(), where we need to build a TokenizedAuthLine when
the others have already done so tokenize_file_with_context(). Could
it be cleaner to have a small routine like makeTokenizedAuthLine()
that gets reused when we fail scanning a directory to build a
TokenizedAuthLine, in combination with a small-ish routine working on
a directory like ParseConfigDirectory() but for HBA/ident? Or we
could just drop process_included_authfile() entirely? On failure,
this would make the code do a next_line all the time for all the
include clauses.

I have been waiting for your reply for some time, so I have taken some
to look at this patch by myself and hacked on it.

At the end, the thing I was not really happy about is the
MemoryContext used to store the set of TokenizedAuthLines. I have
looked at a couple of approaches, like passing around the context as
you do, but at the end there is something I found annoying: we may
tokenize a file in the line context of a different file, storing in
much more data than just the TokenizedAuthLines. Then I got a few
steps back and began using a static memory context that only stored
the TokenizedAuthLines, switching to it in one place as of the end of
tokenize_auth_file() when inserting an item. This makes hbafuncs.c a
bit less aware of the memory context, but I think that we could live
with that with a cleaner tokenization interface. That's close to what
GUCs do, in some way. Note that this may be better as an independent
patch, actually, as it impacts the current @ inclusions.

I have noticed a bug in the logic of include_if_exists: we should
ignore only files on ENOENT, but complain for other errnos after
opening the file.

The docs and the sample files have been tweaked a bit, giving a
cleaner separation between the main record types and the inclusion
ones.

+     /* XXX: this should stick to elevel for some cases? */
+     ereport(LOG,
+             (errmsg("skipping missing authentication file \"%s\"",
+                     inc_fullname)));
Should we always issue a LOG here?  In some cases we use an elevel of
DEBUG3.

So, what do you think about something like the attached? I have begun
a lookup at the tests, but I don't have enough material for an actual
review of this part yet. Note that I have removed temporarily
process_included_authfile(), as I was looking at all the code branches
in details. The final result ought to include AbsoluteConfigLocation(),
open_auth_file() and tokenize_auth_file() with an extra missing_ok
argument, I guess ("strict" as proposed originally is not the usual
PG-way for ENOENT-ish problems). process_line should be used only
when we have no err_msg, meaning that these have been consumed in some
TokenizedAuthLines already.
--
Michael

Attachments:

v20-0001-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/x-diff; charset=us-asciiDownload
From c585c8d4dffb99f6abb4129cd9adf27849fa0ce0 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 14 Nov 2022 13:47:15 +0900
Subject: [PATCH v20 1/2] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/backend/libpq/hba.c                       | 233 ++++++-
 src/backend/libpq/pg_hba.conf.sample          |  25 +-
 src/backend/libpq/pg_ident.conf.sample        |  15 +-
 src/backend/utils/adt/hbafuncs.c              |  39 +-
 .../authentication/t/004_file_inclusion.pl    | 657 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 doc/src/sgml/client-auth.sgml                 |  86 ++-
 doc/src/sgml/system-views.sgml                |  23 +-
 9 files changed, 1020 insertions(+), 76 deletions(-)
 create mode 100644 src/test/authentication/t/004_file_inclusion.pl

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f15aa2dbb1..f9301b2627 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6161,16 +6161,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{map_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{map_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index abdebeb3f8..d5d5c111bc 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -76,6 +76,12 @@ typedef struct
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+typedef enum HbaIncludeKind
+{
+	SecondaryAuthFile,
+	IncludedAuthFile
+} HbaIncludeKind;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -121,6 +127,10 @@ static const char *const UserAuthName[] =
 };
 
 
+static void tokenize_file_with_context(MemoryContext linecxt,
+									   const char *filename, FILE *file,
+									   List **tok_lines, int depth,
+									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel,
 							   int depth, char **err_msg);
@@ -131,6 +141,10 @@ static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
 static void tokenize_error_callback(void *arg);
+static char *process_included_authfile(const char *inc_filename, bool strict,
+									   const char *outer_filename, int depth,
+									   int elevel, MemoryContext linecxt,
+									   List **tok_lines);
 
 
 /*
@@ -590,11 +604,38 @@ tokenize_error_callback(void *arg)
 
 /*
  * tokenize_auth_file
- *		Tokenize the given file.
+ *
+ * Wrapper around tokenize_file_with_context, creating a dedicated memory
+ * context.
+ *
+ * Return value is this memory context which contains all memory allocated by
+ * this function (it's a child of caller's context).
+ */
+MemoryContext
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int depth, int elevel)
+{
+	MemoryContext linecxt;
+	linecxt = AllocSetContextCreate(CurrentMemoryContext,
+									"tokenize_auth_file",
+									ALLOCSET_SMALL_SIZES);
+
+	*tok_lines = NIL;
+
+	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
+							   elevel);
+
+	return linecxt;
+}
+
+/*
+ * Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
+ * linecxt: memory context which must contain all memory allocated by the
+ * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -604,17 +645,13 @@ tokenize_error_callback(void *arg)
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int elevel, int depth)
+static void
+tokenize_file_with_context(MemoryContext linecxt, const char *filename,
+						   FILE *file, List **tok_lines, int elevel, int depth)
 {
-	int			line_number = 1;
 	StringInfoData buf;
-	MemoryContext linecxt;
+	int			line_number = 1;
 	MemoryContext oldcxt;
 	ErrorContextCallback tokenerrcontext;
 	tokenize_error_callback_arg callback_arg;
@@ -627,17 +664,13 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 	tokenerrcontext.previous = error_context_stack;
 	error_context_stack = &tokenerrcontext;
 
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
 	oldcxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
-
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -698,21 +731,121 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedAuthLine *tok_line;
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
 
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->file_name = pstrdup(filename);
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
-			*tok_lines = lappend(*tok_lines, tok_line);
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			AuthToken *first, *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, true,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetConfFilesInDir(dir_name, filename, elevel,
+						&num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* We have the error in err_msg, simply process it */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					/*
+					 * err_msg is used here as a temp buffer, it will be
+					 * overwritten at the end of the loop with the
+					 * cumulated errors, if any.
+					 */
+					err_msg = process_included_authfile(filenames[i], true,
+												filename, depth + 1, elevel,
+												linecxt, tok_lines);
+
+					/* Cumulate errors if any. */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/*
+				 * If there were no errors, the line is fully processed, bypass
+				 * the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+				char	   *inc_filename;
+
+				inc_filename = second->string;
+
+				err_msg = process_included_authfile(inc_filename, false,
+										  filename, depth + 1, elevel, linecxt,
+										  tok_lines);
+
+				if (!err_msg)
+				{
+					/*
+					 * The line is fully processed, bypass the general
+					 * TokenizedAuthLine processing.
+					 */
+					goto next_line;
+				}
+			}
 		}
 
+process_line:
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine
+		*/
+		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg;
+		*tok_lines = lappend(*tok_lines, tok_line);
+
+next_line:
 		line_number += continuations + 1;
 		callback_arg.linenum = line_number;
 	}
@@ -720,11 +853,8 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 	MemoryContextSwitchTo(oldcxt);
 
 	error_context_stack = tokenerrcontext.previous;
-
-	return linecxt;
 }
 
-
 /*
  * Does user belong to role?
  *
@@ -2511,6 +2641,53 @@ load_hba(void)
 }
 
 
+/*
+ * Try to open an included file, and tokenize it using the given context.
+ * Returns NULL if no error happens during tokenization, otherwise the error.
+ */
+static char *
+process_included_authfile(const char *inc_filename, bool strict,
+						  const char *outer_filename, int depth, int elevel,
+						  MemoryContext linecxt, List **tok_lines)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+	char	   *err_msg = NULL;
+
+	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, &err_msg);
+
+	if (inc_file == NULL)
+	{
+		if (strict)
+		{
+			/* open_auth_file should have reported an error. */
+			Assert(err_msg != NULL);
+			return err_msg;
+		}
+		else
+		{
+			ereport(LOG,
+					(errmsg("skipping missing authentication file \"%s\"",
+							inc_fullname)));
+			return NULL;
+		}
+	}
+	else
+	{
+		/* No error message should have been reported. */
+		Assert(err_msg == NULL);
+	}
+
+	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
+							   tok_lines, elevel, depth);
+
+	FreeFile(inc_file);
+	pfree(inc_fullname);
+
+	return NULL;
+}
+
 /*
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..7433050112 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -9,16 +9,27 @@
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# local         DATABASE  USER  METHOD  [OPTIONS]
-# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# local             DATABASE  USER  METHOD  [OPTIONS]
+# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# The first field is the connection type:
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
+# Otherwise the first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..8e3fa29135 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -7,12 +7,23 @@
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are of the form:
+# are one of these forms:
 #
-# MAPNAME  SYSTEM-USERNAME  PG-USERNAME
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+# MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index b662e7b55f..f9c99d41c6 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int map_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int map_number, char *filename, int lineno,
+							IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, char *filename,int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -203,6 +204,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(rule_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -346,7 +350,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -404,7 +408,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			rule_number++;
 
 		fill_hba_line(tuple_store, tupdesc, rule_number,
-					  tok_line->line_num, hbaline, tok_line->err_msg);
+					  tok_line->file_name, tok_line->line_num, hbaline,
+					  tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -441,7 +446,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -450,7 +455,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * map_number: unique identifier among all valid maps
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -459,7 +465,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int map_number, int lineno, IdentLine *ident,
+				int map_number, char *filename, int lineno, IdentLine *ident,
 				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -479,6 +485,9 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(map_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -491,7 +500,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -548,8 +557,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			map_number++;
 
 		fill_ident_line(tuple_store, tupdesc, map_number,
-						tok_line->line_num, identline,
-						tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num,
+						identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
new file mode 100644
index 0000000000..4d8d463d15
--- /dev/null
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -0,0 +1,657 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g; # reduce /./ to /
+$data_dir =~ s/\/\//\//g;   # reduce // to /
+$data_dir =~ s/\/$//;       # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}'; # database
+	$tokens[2] = '{' . $tokens[2] . '}'; # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [$filename, $fileline, $err_str];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [$filename, $fileline, $err_str];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node       = shift;
+	my $hba_expected = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres', "ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres', "ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf', "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .= add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf', "local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .= add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf', "ignore - should not be included");
+	$hba_expected .= add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .= add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n" . ($cur_line{'hba_rule'} - 1)
+		. "|$data_dir/$hba_file|" . ($cur_line{$hba_file} - 1)
+		. '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .= add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file", "include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "include pg_ident_pos2.conf");
+	$ident_expected .= add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file", "include_if_exists ../ident_inc_if/some");
+	$ident_expected .= add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf', "ignore - should not be included");
+	$ident_expected .= add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .= add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node       = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if ($err_str !~ /maximum nesting depth exceeded/ and
+			$err_str !~ /could not open file/)
+		{
+			push @errors, qr/line $fileline of configuration file "$data_dir\/$filename"/
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str"
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+		PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents,
+			$_,
+			"Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+			PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql(
+		'postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0),
+	'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba),
+	'pg_hba_file_rules content is expected');
+
+is($node->safe_psql(
+		'postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident),
+	'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like($node, $hba_file, "include ../not_a_file",
+	[
+		qr/could not open file "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like($node, $hba_file, "include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	], 1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1, add_hba_line($node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file, "include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2, add_hba_line($node, "hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open file \"$data_dir/dbnames_fails.conf\": No such file or directory");
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2, add_hba_line($node, "recurse.conf",
+	"include recurse.conf",
+	"could not open file \"$data_dir/recurse.conf\": maximum nesting depth exceeded");
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors = generate_log_err_patterns($node, \@hba_raw_errors_step2,
+	1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+	qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like(
+	$node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1, add_ident_line($node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file, "include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0),
+	0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open file \"$data_dir/osnames_fails.conf\": No such file or directory");
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2, add_ident_line($node, "ident_recurse.conf", "include ident_recurse.conf",
+	"could not open file \"$data_dir/ident_recurse.conf\": maximum nesting depth exceeded");
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is($node->safe_psql(
+	'postgres',
+	'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors, add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql('postgres', 'SELECT 1',
+	stdout => \$stdout, stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like($stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag("Build not compatible with auth file view error reporting, bail out.\n");
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+	. ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors, add_ident_line($node, $ident_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory");
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is($node->safe_psql(
+	'postgres',
+	'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+	. ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 624d0e5aae..4c6c25dbb6 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1338,6 +1338,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1347,14 +1348,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.map_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(map_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(map_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..2ae723de66 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,23 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each record can either be an inclusion directive or an authentication
+   record.  Inclusion directives specify files that can be included, which
+   contains additional records.  The records will be inserted in lieu of the
+   inclusion records.  Those records only contains two fields: the
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double quoted if needed.  For the <literal>include_dir</literal> form,
+   all files not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
+  <para>
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -103,21 +118,57 @@
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
+
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -863,8 +914,10 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
@@ -875,6 +928,11 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can either
+   be inclusion directives or user name map records, and follow the same
+   rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7c716fe327..a21c3fee15 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1002,12 +1002,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule the given <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1152,12 +1161,22 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this map
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this map in <filename>pg_ident.conf</filename>
+       Line number of this map in the corresponding
+       <literal>file_name</literal>
       </para></entry>
      </row>
 
-- 
2.38.1

v20-0002-My-own-changes.patchtext/x-diff; charset=us-asciiDownload
From a3f02d723274e643741a12c106e9440e7229401d Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Tue, 22 Nov 2022 17:18:05 +0900
Subject: [PATCH v20 2/2] My own changes

---
 src/include/libpq/hba.h                |   6 +-
 src/backend/libpq/hba.c                | 272 ++++++++++++-------------
 src/backend/libpq/pg_hba.conf.sample   |  52 +++--
 src/backend/libpq/pg_ident.conf.sample |  31 ++-
 src/backend/utils/adt/hbafuncs.c       |  12 +-
 src/test/authentication/meson.build    |   1 +
 doc/src/sgml/client-auth.sgml          | 109 +++++-----
 doc/src/sgml/system-views.sgml         |   5 +-
 8 files changed, 257 insertions(+), 231 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index a84a5f0961..b1f2d8410d 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -179,7 +179,9 @@ extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern FILE *open_auth_file(const char *filename, int elevel, int depth,
 							char **err_msg);
-extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel, int depth);
+extern void tokenize_auth_file(const char *filename, FILE *file,
+							   List **tok_lines, int elevel, int depth);
+extern void tokenize_init_context(void);
+extern void tokenize_reset_context(void);
 
 #endif							/* HBA_H */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index d5d5c111bc..4382e5a7d1 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -76,11 +76,12 @@ typedef struct
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
-typedef enum HbaIncludeKind
-{
-	SecondaryAuthFile,
-	IncludedAuthFile
-} HbaIncludeKind;
+/*
+ * Memory context holding the list of TokenizedAuthLines when parsing
+ * HBA or ident config files.  This is created at the top point loading
+ * HBA or ident files.
+ */
+static MemoryContext tokenize_context = NULL;
 
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
@@ -127,10 +128,6 @@ static const char *const UserAuthName[] =
 };
 
 
-static void tokenize_file_with_context(MemoryContext linecxt,
-									   const char *filename, FILE *file,
-									   List **tok_lines, int depth,
-									   int elevel);
 static List *tokenize_inc_file(List *tokens, const char *outer_filename,
 							   const char *inc_filename, int elevel,
 							   int depth, char **err_msg);
@@ -141,10 +138,6 @@ static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
 static int	regexec_auth_token(const char *match, AuthToken *token,
 							   size_t nmatch, regmatch_t pmatch[]);
 static void tokenize_error_callback(void *arg);
-static char *process_included_authfile(const char *inc_filename, bool strict,
-									   const char *outer_filename, int depth,
-									   int elevel, MemoryContext linecxt,
-									   List **tok_lines);
 
 
 /*
@@ -485,9 +478,8 @@ tokenize_inc_file(List *tokens,
 {
 	char	   *inc_fullname;
 	FILE	   *inc_file;
-	List	   *inc_lines;
+	List	   *inc_lines = NIL;
 	ListCell   *inc_line;
-	MemoryContext linecxt;
 
 	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
 	inc_file = open_auth_file(inc_fullname, elevel, depth, err_msg);
@@ -500,13 +492,16 @@ tokenize_inc_file(List *tokens,
 	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel,
-								 depth);
+	tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel,
+					   depth);
 
 	FreeFile(inc_file);
 	pfree(inc_fullname);
 
-	/* Copy all tokens found in the file and append to the tokens list */
+	/*
+	 * Move all the tokens found in the file to the tokens list.  These
+	 * are already saved in tokenize_context.
+	 */
 	foreach(inc_line, inc_lines)
 	{
 		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(inc_line);
@@ -528,12 +523,11 @@ tokenize_inc_file(List *tokens,
 			{
 				AuthToken  *token = lfirst(inc_token);
 
-				tokens = lappend(tokens, copy_auth_token(token));
+				tokens = lappend(tokens, token);
 			}
 		}
 	}
 
-	MemoryContextDelete(linecxt);
 	return tokens;
 }
 
@@ -584,6 +578,9 @@ open_auth_file(const char *filename, int elevel, int depth,
 		if (err_msg)
 			*err_msg = psprintf("could not open file \"%s\": %s",
 								filename, strerror(save_errno));
+
+		/* the caller may care about some specific errno */
+		errno = save_errno;
 		return NULL;
 	}
 
@@ -602,40 +599,32 @@ tokenize_error_callback(void *arg)
 			   callback_arg->linenum, callback_arg->filename);
 }
 
-/*
- * tokenize_auth_file
- *
- * Wrapper around tokenize_file_with_context, creating a dedicated memory
- * context.
- *
- * Return value is this memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
- */
-MemoryContext
-tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
-				   int depth, int elevel)
+void
+tokenize_init_context(void)
 {
-	MemoryContext linecxt;
-	linecxt = AllocSetContextCreate(CurrentMemoryContext,
-									"tokenize_auth_file",
-									ALLOCSET_SMALL_SIZES);
+	/*
+	 * A context may be present, but assume that it has been eliminated
+	 * already.
+	 * */
+	tokenize_context = AllocSetContextCreate(CurrentMemoryContext,
+											 "tokenize_context",
+											 ALLOCSET_START_SMALL_SIZES);
+}
 
-	*tok_lines = NIL;
-
-	tokenize_file_with_context(linecxt, filename, file, tok_lines, depth,
-							   elevel);
-
-	return linecxt;
+void
+tokenize_reset_context(void)
+{
+	MemoryContextDelete(tokenize_context);
+	tokenize_context = NULL;
 }
 
 /*
- * Tokenize the given file.
+ * tokenize_auth_file
+ *		Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
  * in libpq/hba.h.
  *
- * linecxt: memory context which must contain all memory allocated by the
- * function
  * filename: the absolute path to the target file
  * file: the already-opened target file
  * tok_lines: receives output list
@@ -646,9 +635,9 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
  */
-static void
-tokenize_file_with_context(MemoryContext linecxt, const char *filename,
-						   FILE *file, List **tok_lines, int elevel, int depth)
+void
+tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
+				   int elevel, int depth)
 {
 	StringInfoData buf;
 	int			line_number = 1;
@@ -656,6 +645,8 @@ tokenize_file_with_context(MemoryContext linecxt, const char *filename,
 	ErrorContextCallback tokenerrcontext;
 	tokenize_error_callback_arg callback_arg;
 
+	Assert(tokenize_context);
+
 	callback_arg.filename = filename;
 	callback_arg.linenum = line_number;
 
@@ -664,8 +655,6 @@ tokenize_file_with_context(MemoryContext linecxt, const char *filename,
 	tokenerrcontext.previous = error_context_stack;
 	error_context_stack = &tokenerrcontext;
 
-	oldcxt = MemoryContextSwitchTo(linecxt);
-
 	initStringInfo(&buf);
 
 	while (!feof(file) && !ferror(file))
@@ -747,22 +736,31 @@ tokenize_file_with_context(MemoryContext linecxt, const char *filename,
 
 			if (strcmp(first->string, "include") == 0)
 			{
-				char	   *inc_filename;
+				char	   *inc_fullname;
+				FILE	   *inc_file;
 
-				inc_filename = second->string;
+				inc_fullname = AbsoluteConfigLocation(second->string, filename);
+				inc_file = open_auth_file(inc_fullname, elevel, depth + 1,
+										  &err_msg);
 
-				err_msg = process_included_authfile(inc_filename, true,
-										  filename, depth + 1, elevel, linecxt,
-										  tok_lines);
-
-				if (!err_msg)
+				if (!inc_file)
 				{
-					/*
-					 * The line is fully processed, bypass the general
-					 * TokenizedAuthLine processing.
-					 */
-					goto next_line;
+					/* error in err_msg, so create an entry */
+					pfree(inc_fullname);
+					Assert(err_msg);
+					goto process_line;
 				}
+
+				tokenize_auth_file(inc_fullname, inc_file, tok_lines, elevel,
+								   depth + 1);
+				FreeFile(inc_file);
+				pfree(inc_fullname);
+
+				/*
+				 * tokenize_auth_file() has taken care of creating the
+				 * TokenizedAuthLines, so move on.
+				 */
+				goto next_line;
 			}
 			else if (strcmp(first->string, "include_dir") == 0)
 			{
@@ -776,23 +774,36 @@ tokenize_file_with_context(MemoryContext linecxt, const char *filename,
 
 				if (!filenames)
 				{
-					/* We have the error in err_msg, simply process it */
+					/* the error is in err_msg, so create an entry */
 					goto process_line;
 				}
 
 				initStringInfo(&err_buf);
 				for (int i = 0; i < num_filenames; i++)
 				{
-					/*
-					 * err_msg is used here as a temp buffer, it will be
-					 * overwritten at the end of the loop with the
-					 * cumulated errors, if any.
-					 */
-					err_msg = process_included_authfile(filenames[i], true,
-												filename, depth + 1, elevel,
-												linecxt, tok_lines);
+					char	   *inc_fullname;
+					FILE	   *inc_file;
 
-					/* Cumulate errors if any. */
+					inc_fullname = AbsoluteConfigLocation(filenames[i], filename);
+					inc_file = open_auth_file(inc_fullname, elevel, depth + 1,
+											  &err_msg);
+
+					if (!inc_file)
+					{
+						/*
+						 * One of the files has failed, so report it
+						 * and ignore the rest.
+						 */
+						goto process_line;
+					}
+
+					tokenize_auth_file(inc_fullname, inc_file, tok_lines, elevel,
+									   depth + 1);
+
+					FreeFile(inc_file);
+					pfree(inc_fullname);
+
+					/* cumulate errors if any */
 					if (err_msg)
 					{
 						if (err_buf.len > 0)
@@ -810,48 +821,70 @@ tokenize_file_with_context(MemoryContext linecxt, const char *filename,
 
 				/* Otherwise, process the cumulated errors, if any. */
 				err_msg = err_buf.data;
+				goto process_line;
 			}
 			else if (strcmp(first->string, "include_if_exists") == 0)
 			{
-				char	   *inc_filename;
+				char	   *inc_fullname;
+				FILE	   *inc_file;
 
-				inc_filename = second->string;
+				inc_fullname = AbsoluteConfigLocation(second->string, filename);
+				inc_file = open_auth_file(inc_fullname, elevel, depth + 1,
+										  &err_msg);
 
-				err_msg = process_included_authfile(inc_filename, false,
-										  filename, depth + 1, elevel, linecxt,
-										  tok_lines);
-
-				if (!err_msg)
+				if (!inc_file)
 				{
-					/*
-					 * The line is fully processed, bypass the general
-					 * TokenizedAuthLine processing.
-					 */
-					goto next_line;
+					if (errno == ENOENT)
+					{
+						/* no file, so move to next line */
+
+						/* XXX: this should stick to elevel for some cases? */
+						ereport(LOG,
+								(errmsg("skipping missing authentication file \"%s\"",
+										inc_fullname)));
+						pfree(inc_fullname);
+						goto next_line;
+					}
+
+					pfree(inc_fullname);
+					Assert(err_msg);
+					goto process_line;
 				}
+
+				tokenize_auth_file(inc_fullname, inc_file, tok_lines, elevel,
+								   depth + 1);
+				FreeFile(inc_file);
+				pfree(inc_fullname);
+
+				/*
+				 * tokenize_auth_file() has taken care of creating the
+				 * TokenizedAuthLines.
+				 */
+				goto next_line;
 			}
 		}
 
 process_line:
 		/*
 		 * General processing: report the error if any and emit line to the
-		 * TokenizedAuthLine
-		*/
-		tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
+		 * TokenizedAuthLine.  This is saved in the memory context dedicated
+		 * to this list.
+		 */
+		oldcxt = MemoryContextSwitchTo(tokenize_context);
+		tok_line = (TokenizedAuthLine *) palloc0(sizeof(TokenizedAuthLine));
 		tok_line->fields = current_line;
 		tok_line->file_name = pstrdup(filename);
 		tok_line->line_num = line_number;
 		tok_line->raw_line = pstrdup(buf.data);
-		tok_line->err_msg = err_msg;
+		tok_line->err_msg = err_msg ? pstrdup(err_msg) : NULL;
 		*tok_lines = lappend(*tok_lines, tok_line);
+		MemoryContextSwitchTo(oldcxt);
 
 next_line:
 		line_number += continuations + 1;
 		callback_arg.linenum = line_number;
 	}
 
-	MemoryContextSwitchTo(oldcxt);
-
 	error_context_stack = tokenerrcontext.previous;
 }
 
@@ -2539,7 +2572,6 @@ load_hba(void)
 	ListCell   *line;
 	List	   *new_parsed_lines = NIL;
 	bool		ok = true;
-	MemoryContext linecxt;
 	MemoryContext oldcxt;
 	MemoryContext hbacxt;
 
@@ -2550,7 +2582,8 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG, 0);
+	tokenize_init_context();
+	tokenize_auth_file(HbaFileName, file, &hba_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -2602,7 +2635,7 @@ load_hba(void)
 	}
 
 	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
+	tokenize_reset_context();
 	MemoryContextSwitchTo(oldcxt);
 
 	if (!ok)
@@ -2641,53 +2674,6 @@ load_hba(void)
 }
 
 
-/*
- * Try to open an included file, and tokenize it using the given context.
- * Returns NULL if no error happens during tokenization, otherwise the error.
- */
-static char *
-process_included_authfile(const char *inc_filename, bool strict,
-						  const char *outer_filename, int depth, int elevel,
-						  MemoryContext linecxt, List **tok_lines)
-{
-	char	   *inc_fullname;
-	FILE	   *inc_file;
-	char	   *err_msg = NULL;
-
-	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
-	inc_file = open_auth_file(inc_fullname, elevel, depth, &err_msg);
-
-	if (inc_file == NULL)
-	{
-		if (strict)
-		{
-			/* open_auth_file should have reported an error. */
-			Assert(err_msg != NULL);
-			return err_msg;
-		}
-		else
-		{
-			ereport(LOG,
-					(errmsg("skipping missing authentication file \"%s\"",
-							inc_fullname)));
-			return NULL;
-		}
-	}
-	else
-	{
-		/* No error message should have been reported. */
-		Assert(err_msg == NULL);
-	}
-
-	tokenize_file_with_context(linecxt, inc_fullname, inc_file,
-							   tok_lines, elevel, depth);
-
-	FreeFile(inc_file);
-	pfree(inc_fullname);
-
-	return NULL;
-}
-
 /*
  * Parse one tokenised line from the ident config file and store the result in
  * an IdentLine structure.
@@ -2953,7 +2939,6 @@ load_ident(void)
 			   *parsed_line_cell;
 	List	   *new_parsed_lines = NIL;
 	bool		ok = true;
-	MemoryContext linecxt;
 	MemoryContext oldcxt;
 	MemoryContext ident_context;
 	IdentLine  *newline;
@@ -2966,7 +2951,8 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG, 0);
+	tokenize_init_context();
+	tokenize_auth_file(IdentFileName, file, &ident_lines, LOG, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -3003,7 +2989,7 @@ load_ident(void)
 	}
 
 	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
+	tokenize_reset_context();
 	MemoryContextSwitchTo(oldcxt);
 
 	if (!ok)
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 7433050112..f72f471d23 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -5,31 +5,24 @@
 # documentation for a complete description of this file.  A short
 # synopsis follows.
 #
+# -------------------------------
+# Authentication records
+# -------------------------------
+#
 # This file controls: which hosts are allowed to connect, how clients
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
 #
-# include           FILE
-# include_if_exists FILE
-# include_dir       DIRECTORY
-# local             DATABASE  USER  METHOD  [OPTIONS]
-# host              DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostssl           DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnossl         DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostgssenc        DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
-# hostnogssenc      DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# local         DATABASE  USER  METHOD  [OPTIONS]
+# host          DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostssl       DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnossl     DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostgssenc    DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
+# hostnogssenc  DATABASE  USER  ADDRESS  METHOD  [OPTIONS]
 #
 # (The uppercase items must be replaced by actual values.)
 #
-# If the first field is "include", "include_if_exists" or "include_dir", it's
-# not a mapping record but a directive to include records from respectively
-# another file, another file if it exists or all the files in the given
-# directory ending in '.conf'.  FILE is the file name to include, and
-# DIR is the directory name containing the file(s) to include. FILE and
-# DIRECTORY can be specified with a relative or absolute path, and can be
-# double quoted if they contains spaces.
-#
-# Otherwise the first field is the connection type:
+# The first field is the connection type:
 # - "local" is a Unix-domain socket
 # - "host" is a TCP/IP socket (encrypted or not)
 # - "hostssl" is a TCP/IP socket that is SSL-encrypted
@@ -75,11 +68,34 @@
 # its special character, and just match a database or username with
 # that name.
 #
+# --------------------------------
+# Inclusion records
+# ---------------------------------
+#
+# This files allow the inclusion of external files or directories holding
+# extra authentication records, using the following keywords:
+#
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+#
+# FILE is the file name to include, and DIR is the directory name containing
+# the file(s) to include.  Any file in a directory will be loaded if suffixed
+# with ".conf".  The files of a directory are ordered by name.
+# include_if_exists ignored missing files.  FILE and DIRECTORY can be
+# specified as a relative or absolute path, and can be double-quoted if they
+# contain spaces.
+#
+# -------------------------------
+# Miscellaneous
+# -------------------------------
+#
 # This file is read on server startup and when the server receives a
 # SIGHUP signal.  If you edit the file on a running system, you have to
 # SIGHUP the server for the changes to take effect, run "pg_ctl reload",
 # or execute "SELECT pg_reload_conf()".
 #
+# ---------------------------------
 # Put your actual configuration here
 # ----------------------------------
 #
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index 8e3fa29135..8d9b028aa3 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -1,17 +1,18 @@
 # PostgreSQL User Name Maps
 # =========================
 #
+# ------------------------------
+# Ident Records
+# ------------------------------
+#
 # Refer to the PostgreSQL documentation, chapter "Client
 # Authentication" for a complete description.  A short synopsis
 # follows.
 #
 # This file controls PostgreSQL user name mapping.  It maps external
 # user names to their corresponding PostgreSQL user names.  Records
-# are one of these forms:
+# are of the form:
 #
-# include           FILE
-# include_if_exists FILE
-# include_dir       DIRECTORY
 # MAPNAME           SYSTEM-USERNAME  PG-USERNAME
 #
 # (The uppercase quantities must be replaced by actual values.)
@@ -42,6 +43,28 @@
 # system user names and PostgreSQL user names are the same, you don't
 # need anything in this file.
 #
+# ------------------------------
+# Inclusion records
+# ------------------------------
+#
+# This files allow the inclusion of external files or directories holding
+# extra records, using the following keywords:
+#
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+#
+# FILE is the file name to include, and DIR is the directory name containing
+# the file(s) to include.  Any file in a directory will be loaded if suffixed
+# with ".conf".  The files of a directory are ordered by name.
+# include_if_exists ignored missing files.  FILE and DIRECTORY can be
+# specified as a relative or absolute path, and can be double-quoted if they
+# contain spaces.
+#
+# -------------------------------
+# Miscellaneous
+# -------------------------------
+#
 # This file is read on server startup and when the postmaster receives
 # a SIGHUP signal.  If you edit the file on a running system, you have
 # to SIGHUP the postmaster for the changes to take effect.  You can
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index f9c99d41c6..bab6c3176c 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -374,7 +374,6 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	List	   *hba_lines = NIL;
 	ListCell   *line;
 	int			rule_number = 0;
-	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
 
@@ -386,7 +385,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 */
 	file = open_auth_file(HbaFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3, 0);
+	tokenize_init_context();
+	tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -413,7 +413,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	}
 
 	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
+	tokenize_reset_context();
 	/* Free parse_hba_line memory */
 	MemoryContextSwitchTo(oldcxt);
 	MemoryContextDelete(hbacxt);
@@ -523,7 +523,6 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	List	   *ident_lines = NIL;
 	ListCell   *line;
 	int			map_number = 0;
-	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
 
@@ -534,8 +533,9 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 * entry.)
 	 */
 	file = open_auth_file(IdentFileName, ERROR, 0, NULL);
+	tokenize_init_context();
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3, 0);
+	tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3, 0);
 	FreeFile(file);
 
 	/* Now parse all the lines */
@@ -562,7 +562,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	}
 
 	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
+	tokenize_reset_context();
 	/* Free parse_ident_line memory */
 	MemoryContextSwitchTo(oldcxt);
 	MemoryContextDelete(identcxt);
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index c2b48c43c9..cfc23fa213 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -7,6 +7,7 @@ tests += {
       't/001_password.pl',
       't/002_saslprep.pl',
       't/003_peer.pl',
+      't/004_file_inclusion.pl',
     ],
   },
 }
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 2ae723de66..e5815a5390 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -88,21 +88,6 @@
    Backslash line continuation applies even within quoted text or comments.
   </para>
 
-  <para>
-   Each record can either be an inclusion directive or an authentication
-   record.  Inclusion directives specify files that can be included, which
-   contains additional records.  The records will be inserted in lieu of the
-   inclusion records.  Those records only contains two fields: the
-   <literal>include</literal>, <literal>include_if_exists</literal> or
-   <literal>include_dir</literal> directive and the file or directory to be
-   included.  The file or directory can be a relative of absolute path, and can
-   be double quoted if needed.  For the <literal>include_dir</literal> form,
-   all files not starting with a <literal>.</literal> and ending with
-   <literal>.conf</literal> will be included.  Multiple files within an include
-   directory are processed in file name order (according to C locale rules,
-   i.e., numbers before letters, and uppercase letters before lowercase ones).
-  </para>
-
   <para>
    Each authentication record specifies a connection type, a client IP address
    range (if relevant for the connection type), a database name, a user name,
@@ -115,12 +100,24 @@
    access is denied.
   </para>
 
+  <para>
+   Each record can be an inclusion directive or an authentication record.
+   Inclusion directives specify files that can be included, that contain
+   additional records.  The records will be inserted in place of the
+   inclusion records.  Those records only contains two fields:
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double-quoted.  For the <literal>include_dir</literal> form, all files
+   not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
   <para>
    A record can have several formats:
 <synopsis>
-include             <replaceable>file</replaceable>
-include_if_exists   <replaceable>file</replaceable>
-include_dir         <replaceable>directory</replaceable>
 local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
 host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
@@ -132,43 +129,13 @@ hostssl             <replaceable>database</replaceable>  <replaceable>user</repl
 hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
 hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
 </synopsis>
    The meaning of the fields is as follows:
 
    <variablelist>
-    <varlistentry>
-     <term><literal>include</literal></term>
-     <listitem>
-      <para>
-       This line will be replaced with the content of the given file.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>include_if_exists</literal></term>
-     <listitem>
-      <para>
-       This line will be replaced with the content of the given file if the
-       file exists and can be read.  Otherwise, a message will be logged to
-       indicate that the file is skipped.
-      </para>
-     </listitem>
-    </varlistentry>
-
-    <varlistentry>
-     <term><literal>include_dir</literal></term>
-     <listitem>
-      <para>
-       This line will be replaced with the content of all the files found in
-       the directory, if they don't start with a <literal>.</literal> and end
-       with <literal>.conf</literal>, processed in file name order (according
-       to C locale rules, i.e., numbers before letters, and uppercase letters
-       before lowercase ones).
-      </para>
-     </listitem>
-    </varlistentry>
-
     <varlistentry>
      <term><literal>local</literal></term>
      <listitem>
@@ -706,6 +673,39 @@ openssl x509 -in myclient.crt -noout --subject -nameopt RFC2253 | sed "s/^subjec
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced by the contents of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
@@ -916,9 +916,9 @@ local   db1,db2,@demodbs  all                                   md5
    configuration parameter.)
    The ident map file contains lines of two general form:
 <synopsis>
+<replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 <replaceable>include</replaceable> <replaceable>file</replaceable>
 <replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
-<replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
    <filename>pg_hba.conf</filename>.  The
@@ -929,9 +929,8 @@ local   db1,db2,@demodbs  all                                   md5
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
   <para>
-   As for <filename>pg_hba.conf</filename>, the lines in this file can either
-   be inclusion directives or user name map records, and follow the same
-   rules.
+   As for <filename>pg_hba.conf</filename>, the lines in this file can
+   be inclusion directives, following the same rules.
   </para>
   <para>
    There is no restriction regarding how many database users a given
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index a21c3fee15..d38b42c5cd 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1016,7 +1016,7 @@
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule the given <literal>file_name</literal>
+       Line number of this rule in <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1175,8 +1175,7 @@
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this map in the corresponding
-       <literal>file_name</literal>
+       Line number of this map in <literal>file_name</literal>
       </para></entry>
      </row>
 
-- 
2.38.1

#83Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#82)
2 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Tue, Nov 22, 2022 at 05:20:01PM +0900, Michael Paquier wrote:

+     /* XXX: this should stick to elevel for some cases? */
+     ereport(LOG,
+             (errmsg("skipping missing authentication file \"%s\"",
+                     inc_fullname)));
Should we always issue a LOG here?  In some cases we use an elevel of
DEBUG3.

And here I think that we need to use elevel. In hba.c, this would
generate a LOG while hbafuncs.c uses DEBUG3, leading to less noise in
the log files.

So, what do you think about something like the attached? I have begun
a lookup at the tests, but I don't have enough material for an actual
review of this part yet. Note that I have removed temporarily
process_included_authfile(), as I was looking at all the code branches
in details. The final result ought to include AbsoluteConfigLocation(),
open_auth_file() and tokenize_auth_file() with an extra missing_ok
argument, I guess ("strict" as proposed originally is not the usual
PG-way for ENOENT-ish problems). process_line should be used only
when we have no err_msg, meaning that these have been consumed in some
TokenizedAuthLines already.

This, however, was still brittle in terms of memory handling.
Reloading the server a few hundred times proved that this was leaking
memory in the tokenization. This becomes quite simple once you switch
to an approach where tokenize_auth_file() uses its own memory context,
while we store all the TokenizedAuthLines in the static context. It
also occurred to me that we can create and drop the static context
holding the tokens when opening the top-root auth file, aka with a
depth at 0. It may be a bit simpler to switch to a single-context
approach for all the allocations of tokenize_auth_file(), though I
find the use of a static context much cleaner for the inclusions with
@ files when we expand an existing list.

The patch can be split, again, into more pieces. Attached 0001
reworks the memory allocation contexts for the tokenization and 0002
is the main feature. As of things are, I am quite happy with the
shapes of 0001 and 0002. I have tested quite a bit its robustness,
with valgrind for example, to make sure that there are no leaks in the
postmaster (with[out] -DEXEC_BACKEND). The inclusion logic is
refactored to be in a state close to your previous patches, see
tokenize_inclusion_file().

Note that the tests are failing for some of the Windows CIs, actually,
due to a difference in some of the paths generated vs the file paths
(aka c:\cirrus vs c:/cirrus, as far as I saw).
--
Michael

Attachments:

v21-0001-Rework-memory-contexts-in-charge-of-HBA-ident-to.patchtext/x-diff; charset=us-asciiDownload
From 7949bda7adcb378f8355f06325d4fade56077337 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 23 Nov 2022 14:39:07 +0900
Subject: [PATCH v21 1/2] Rework memory contexts in charge of HBA/ident
 tokenization

The list of TokenizedAuthLines generated for the tokens are now stored
in a static context called tokenize_context, where only all the parsed
elements are stored.  This context is stored when opening the top-root
of an authentication file, and is cleaned up once we are done with it
with a new routine called free_auth_file().  One call of
open_auth_file() should have one matching call of free_auth_file().

Rather than having tokenize_auth_file() return a memory context that
includes all the records, this now creates and drops one memory context
each time the function is called.  This will simplify recursive calls to
this routine for the upcoming inclusion record logic.

While on it, rename tokenize_inc_file() to tokenize_expand_file() as
this would conflict with the upcoming patch that will add inclusion
records for HBA/ident files.

Reloading HBA/indent configuration in a tight loop shows no leaks, as of
one type of test done (with and without -DEXEC_BACKEND).
---
 src/include/libpq/hba.h          |   5 +-
 src/backend/libpq/hba.c          | 152 +++++++++++++++++++++++--------
 src/backend/utils/adt/hbafuncs.c |  12 +--
 3 files changed, 119 insertions(+), 50 deletions(-)

diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index a84a5f0961..90c51ad6fa 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -179,7 +179,8 @@ extern IdentLine *parse_ident_line(TokenizedAuthLine *tok_line, int elevel);
 extern bool pg_isblank(const char c);
 extern FILE *open_auth_file(const char *filename, int elevel, int depth,
 							char **err_msg);
-extern MemoryContext tokenize_auth_file(const char *filename, FILE *file,
-										List **tok_lines, int elevel, int depth);
+extern void free_auth_file(FILE *file, int depth);
+extern void tokenize_auth_file(const char *filename, FILE *file,
+							   List **tok_lines, int elevel, int depth);
 
 #endif							/* HBA_H */
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index abdebeb3f8..9064ad6714 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -76,6 +76,13 @@ typedef struct
 #define token_is_keyword(t, k)	(!t->quoted && strcmp(t->string, k) == 0)
 #define token_matches(t, k)  (strcmp(t->string, k) == 0)
 
+/*
+ * Memory context holding the list of TokenizedAuthLines when parsing
+ * HBA or ident config files.  This is created when loading the top HBA
+ + or ident file (depth of 0).
+ */
+static MemoryContext tokenize_context = NULL;
+
 /*
  * pre-parsed content of HBA config file: list of HbaLine structs.
  * parsed_hba_context is the memory context where it lives.
@@ -121,9 +128,9 @@ static const char *const UserAuthName[] =
 };
 
 
-static List *tokenize_inc_file(List *tokens, const char *outer_filename,
-							   const char *inc_filename, int elevel,
-							   int depth, char **err_msg);
+static List *tokenize_expand_file(List *tokens, const char *outer_filename,
+								  const char *inc_filename, int elevel,
+								  int depth, char **err_msg);
 static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
 							   int elevel, char **err_msg);
 static int	regcomp_auth_token(AuthToken *token, char *filename, int line_num,
@@ -437,17 +444,27 @@ next_field_expand(const char *filename, char **lineptr,
 
 		/* Is this referencing a file? */
 		if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
-			tokens = tokenize_inc_file(tokens, filename, buf + 1,
-									   elevel, depth + 1, err_msg);
+			tokens = tokenize_expand_file(tokens, filename, buf + 1,
+										  elevel, depth + 1, err_msg);
 		else
+		{
+			MemoryContext oldcxt;
+
+			/*
+			 * lappend() may do its own allocations, so move to the context
+			 * for the list of tokens.
+			 */
+			oldcxt = MemoryContextSwitchTo(tokenize_context);
 			tokens = lappend(tokens, make_auth_token(buf, initial_quote));
+			MemoryContextSwitchTo(oldcxt);
+		}
 	} while (trailing_comma && (*err_msg == NULL));
 
 	return tokens;
 }
 
 /*
- * tokenize_inc_file
+ * tokenize_expand_file
  *		Expand a file included from another file into an hba "field"
  *
  * Opens and tokenises a file included from another HBA config file with @,
@@ -462,18 +479,17 @@ next_field_expand(const char *filename, char **lineptr,
  * there was an error.
  */
 static List *
-tokenize_inc_file(List *tokens,
-				  const char *outer_filename,
-				  const char *inc_filename,
-				  int elevel,
-				  int depth,
-				  char **err_msg)
+tokenize_expand_file(List *tokens,
+					 const char *outer_filename,
+					 const char *inc_filename,
+					 int elevel,
+					 int depth,
+					 char **err_msg)
 {
 	char	   *inc_fullname;
 	FILE	   *inc_file;
 	List	   *inc_lines;
 	ListCell   *inc_line;
-	MemoryContext linecxt;
 
 	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
 	inc_file = open_auth_file(inc_fullname, elevel, depth, err_msg);
@@ -486,13 +502,15 @@ tokenize_inc_file(List *tokens,
 	}
 
 	/* There is possible recursion here if the file contains @ */
-	linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel,
-								 depth);
+	tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel,
+					   depth);
 
-	FreeFile(inc_file);
 	pfree(inc_fullname);
 
-	/* Copy all tokens found in the file and append to the tokens list */
+	/*
+	 * Move all the tokens found in the file to the tokens list.  These are
+	 * already saved in tokenize_context.
+	 */
 	foreach(inc_line, inc_lines)
 	{
 		TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(inc_line);
@@ -513,16 +531,40 @@ tokenize_inc_file(List *tokens,
 			foreach(inc_token, inc_tokens)
 			{
 				AuthToken  *token = lfirst(inc_token);
+				MemoryContext oldcxt;
 
-				tokens = lappend(tokens, copy_auth_token(token));
+				/*
+				 * lappend() may do its own allocations, so move to the
+				 * context for the list of tokens.
+				 */
+				oldcxt = MemoryContextSwitchTo(tokenize_context);
+				tokens = lappend(tokens, token);
+				MemoryContextSwitchTo(oldcxt);
 			}
 		}
 	}
 
-	MemoryContextDelete(linecxt);
+	free_auth_file(inc_file, depth);
 	return tokens;
 }
 
+/*
+ * free_auth_file
+ *		Free a file opened by open_auth_file().
+ */
+void
+free_auth_file(FILE *file, int depth)
+{
+	FreeFile(file);
+
+	/* If this is the last cleanup, remove the tokenization context */
+	if (depth == 0)
+	{
+		MemoryContextDelete(tokenize_context);
+		tokenize_context = NULL;
+	}
+}
+
 /*
  * open_auth_file
  *		Open the given file.
@@ -558,6 +600,22 @@ open_auth_file(const char *filename, int elevel, int depth,
 		return NULL;
 	}
 
+	/*
+	 * When opening the top-level file, create the memory context used for the
+	 * tokenization.  This will be closed with this file when coming back to
+	 * this level of cleanup.
+	 */
+	if (depth == 0)
+	{
+		/*
+		 * A context may be present, but assume that it has been eliminated
+		 * already.
+		 */
+		tokenize_context = AllocSetContextCreate(CurrentMemoryContext,
+												 "tokenize_context",
+												 ALLOCSET_START_SMALL_SIZES);
+	}
+
 	file = AllocateFile(filename, "r");
 	if (file == NULL)
 	{
@@ -570,6 +628,8 @@ open_auth_file(const char *filename, int elevel, int depth,
 		if (err_msg)
 			*err_msg = psprintf("could not open file \"%s\": %s",
 								filename, strerror(save_errno));
+		/* the caller may care about some specific errno */
+		errno = save_errno;
 		return NULL;
 	}
 
@@ -593,32 +653,34 @@ tokenize_error_callback(void *arg)
  *		Tokenize the given file.
  *
  * The output is a list of TokenizedAuthLine structs; see the struct definition
- * in libpq/hba.h.
+ * in libpq/hba.h.  This is the central pieces in charge of parsing the
+ * authentication files.  All the operations of this function happen in its own
+ * local memory context, easing the cleanup of anything allocated locally.
  *
  * filename: the absolute path to the target file
  * file: the already-opened target file
- * tok_lines: receives output list
+ * tok_lines: receives output list, saved into tokenize_context
  * elevel: message logging level
  * depth: level of recursion when tokenizing the target file
  *
  * Errors are reported by logging messages at ereport level elevel and by
  * adding TokenizedAuthLine structs containing non-null err_msg fields to the
  * output list.
- *
- * Return value is a memory context which contains all memory allocated by
- * this function (it's a child of caller's context).
  */
-MemoryContext
+void
 tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 				   int elevel, int depth)
 {
-	int			line_number = 1;
 	StringInfoData buf;
+	int			line_number = 1;
 	MemoryContext linecxt;
+	MemoryContext funccxt;
 	MemoryContext oldcxt;
 	ErrorContextCallback tokenerrcontext;
 	tokenize_error_callback_arg callback_arg;
 
+	Assert(tokenize_context);
+
 	callback_arg.filename = filename;
 	callback_arg.linenum = line_number;
 
@@ -627,14 +689,19 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 	tokenerrcontext.previous = error_context_stack;
 	error_context_stack = &tokenerrcontext;
 
+	/*
+	 * Do all the local tokenization in its own context, to ease the cleanup
+	 * of any memory allocated while tokenizing.
+	 */
 	linecxt = AllocSetContextCreate(CurrentMemoryContext,
 									"tokenize_auth_file",
 									ALLOCSET_SMALL_SIZES);
-	oldcxt = MemoryContextSwitchTo(linecxt);
+	funccxt = MemoryContextSwitchTo(linecxt);
 
 	initStringInfo(&buf);
 
-	*tok_lines = NIL;
+	if (depth == 0)
+		*tok_lines = NIL;
 
 	while (!feof(file) && !ferror(file))
 	{
@@ -694,7 +761,15 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 											  elevel, depth, &err_msg);
 			/* add field to line, unless we are at EOL or comment start */
 			if (current_field != NIL)
+			{
+				/*
+				 * lappend() may do its own allocations, so move to the
+				 * context for the list of tokens.
+				 */
+				oldcxt = MemoryContextSwitchTo(tokenize_context);
 				current_line = lappend(current_line, current_field);
+				MemoryContextSwitchTo(oldcxt);
+			}
 		}
 
 		/*
@@ -704,24 +779,25 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		{
 			TokenizedAuthLine *tok_line;
 
+			oldcxt = MemoryContextSwitchTo(tokenize_context);
 			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
 			tok_line->fields = current_line;
 			tok_line->file_name = pstrdup(filename);
 			tok_line->line_num = line_number;
 			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg;
+			tok_line->err_msg = err_msg ? pstrdup(err_msg) : NULL;
 			*tok_lines = lappend(*tok_lines, tok_line);
+			MemoryContextSwitchTo(oldcxt);
 		}
 
 		line_number += continuations + 1;
 		callback_arg.linenum = line_number;
 	}
 
-	MemoryContextSwitchTo(oldcxt);
+	MemoryContextSwitchTo(funccxt);
+	MemoryContextDelete(linecxt);
 
 	error_context_stack = tokenerrcontext.previous;
-
-	return linecxt;
 }
 
 
@@ -2409,7 +2485,6 @@ load_hba(void)
 	ListCell   *line;
 	List	   *new_parsed_lines = NIL;
 	bool		ok = true;
-	MemoryContext linecxt;
 	MemoryContext oldcxt;
 	MemoryContext hbacxt;
 
@@ -2420,8 +2495,7 @@ load_hba(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG, 0);
-	FreeFile(file);
+	tokenize_auth_file(HbaFileName, file, &hba_lines, LOG, 0);
 
 	/* Now parse all the lines */
 	Assert(PostmasterContext);
@@ -2472,7 +2546,7 @@ load_hba(void)
 	}
 
 	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
+	free_auth_file(file, 0);
 	MemoryContextSwitchTo(oldcxt);
 
 	if (!ok)
@@ -2776,7 +2850,6 @@ load_ident(void)
 			   *parsed_line_cell;
 	List	   *new_parsed_lines = NIL;
 	bool		ok = true;
-	MemoryContext linecxt;
 	MemoryContext oldcxt;
 	MemoryContext ident_context;
 	IdentLine  *newline;
@@ -2789,8 +2862,7 @@ load_ident(void)
 		return false;
 	}
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG, 0);
-	FreeFile(file);
+	tokenize_auth_file(IdentFileName, file, &ident_lines, LOG, 0);
 
 	/* Now parse all the lines */
 	Assert(PostmasterContext);
@@ -2826,7 +2898,7 @@ load_ident(void)
 	}
 
 	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
+	free_auth_file(file, 0);
 	MemoryContextSwitchTo(oldcxt);
 
 	if (!ok)
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index b662e7b55f..87996da487 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -370,7 +370,6 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	List	   *hba_lines = NIL;
 	ListCell   *line;
 	int			rule_number = 0;
-	MemoryContext linecxt;
 	MemoryContext hbacxt;
 	MemoryContext oldcxt;
 
@@ -382,8 +381,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 */
 	file = open_auth_file(HbaFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3, 0);
-	FreeFile(file);
+	tokenize_auth_file(HbaFileName, file, &hba_lines, DEBUG3, 0);
 
 	/* Now parse all the lines */
 	hbacxt = AllocSetContextCreate(CurrentMemoryContext,
@@ -408,7 +406,7 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	}
 
 	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
+	free_auth_file(file, 0);
 	/* Free parse_hba_line memory */
 	MemoryContextSwitchTo(oldcxt);
 	MemoryContextDelete(hbacxt);
@@ -514,7 +512,6 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	List	   *ident_lines = NIL;
 	ListCell   *line;
 	int			map_number = 0;
-	MemoryContext linecxt;
 	MemoryContext identcxt;
 	MemoryContext oldcxt;
 
@@ -526,8 +523,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	 */
 	file = open_auth_file(IdentFileName, ERROR, 0, NULL);
 
-	linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3, 0);
-	FreeFile(file);
+	tokenize_auth_file(IdentFileName, file, &ident_lines, DEBUG3, 0);
 
 	/* Now parse all the lines */
 	identcxt = AllocSetContextCreate(CurrentMemoryContext,
@@ -553,7 +549,7 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 	}
 
 	/* Free tokenizer memory */
-	MemoryContextDelete(linecxt);
+	free_auth_file(file, 0);
 	/* Free parse_ident_line memory */
 	MemoryContextSwitchTo(oldcxt);
 	MemoryContextDelete(identcxt);
-- 
2.38.1

v21-0002-Allow-file-inclusion-in-pg_hba-and-pg_ident-file.patchtext/x-diff; charset=us-asciiDownload
From 1173e0badf1572816b30a251106a6d4feb8c070c Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Wed, 23 Nov 2022 14:47:17 +0900
Subject: [PATCH v21 2/2] Allow file inclusion in pg_hba and pg_ident files.

pg_hba.conf file now has support for "include", "include_dir" and
"include_if_exists" directives, which work similarly to the same directives in
the postgresql.conf file.

Many regression tests added to cover both the new directives, but also error
detection for the whole pg_hba / pg_ident files.

Catversion is bumped.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud
---
 src/include/catalog/pg_proc.dat               |  12 +-
 src/backend/libpq/hba.c                       | 182 ++++-
 src/backend/libpq/pg_hba.conf.sample          |  27 +
 src/backend/libpq/pg_ident.conf.sample        |  34 +
 src/backend/utils/adt/hbafuncs.c              |  39 +-
 src/test/authentication/meson.build           |   1 +
 .../authentication/t/004_file_inclusion.pl    | 721 ++++++++++++++++++
 src/test/regress/expected/rules.out           |   6 +-
 doc/src/sgml/client-auth.sgml                 |  85 ++-
 doc/src/sgml/system-views.sgml                |  22 +-
 10 files changed, 1075 insertions(+), 54 deletions(-)
 create mode 100644 src/test/authentication/t/004_file_inclusion.pl

diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index f15aa2dbb1..f9301b2627 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6161,16 +6161,16 @@
 { oid => '3401', descr => 'show pg_hba.conf rules',
   proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
+  proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}',
   prosrc => 'pg_hba_file_rules' },
 { oid => '6250', descr => 'show pg_ident.conf mappings',
   proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't',
   provolatile => 'v', prorettype => 'record', proargtypes => '',
-  proallargtypes => '{int4,int4,text,text,text,text}',
-  proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{map_number,line_number,map_name,sys_name,pg_username,error}',
+  proallargtypes => '{int4,text,int4,text,text,text,text}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{map_number,file_name,line_number,map_name,sys_name,pg_username,error}',
   prosrc => 'pg_ident_file_mappings' },
 { oid => '1371', descr => 'view system lock information',
   proname => 'pg_lock_status', prorows => '1000', proretset => 't',
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 9064ad6714..3f94359c4f 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -463,6 +463,64 @@ next_field_expand(const char *filename, char **lineptr,
 	return tokens;
 }
 
+/*
+ * tokenize_inclusion_file
+ *		Include a file from another file into an hba "field".
+ *
+ * Opens and tokenises a file included from another HBA config file with
+ * one of the inclusion records ("include", "include_if_exists" or
+ * "include_dir"), and assign all values found to an existing list of
+ * list of AuthTokens.
+ *
+ * All new tokens are allocated in the memory context dedicated to the
+ * tokenization.
+ *
+ * If missing_ok is true, ignore the missing files.
+ *
+ * In event of an error, log a message at ereport level elevel, and also
+ * set *err_msg to a string describing the error.  Note that the result
+ * may be non-NIL anyway, so *err_msg must be tested to determine whether
+ * there was an error.
+ */
+static void
+tokenize_inclusion_file(const char *outer_filename,
+						const char *inc_filename,
+						List **tok_lines,
+						int elevel,
+						int depth,
+						bool missing_ok,
+						char **err_msg)
+{
+	char	   *inc_fullname;
+	FILE	   *inc_file;
+
+	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
+	inc_file = open_auth_file(inc_fullname, elevel, depth, err_msg);
+
+	if (!inc_file)
+	{
+		if (errno == ENOENT && missing_ok)
+		{
+			ereport(elevel,
+					(errmsg("skipping missing authentication file \"%s\"",
+							inc_fullname)));
+			*err_msg = NULL;
+			pfree(inc_fullname);
+			return;
+		}
+
+		/* error in err_msg, so leave and report */
+		pfree(inc_fullname);
+		Assert(err_msg);
+		return;
+	}
+
+	tokenize_auth_file(inc_fullname, inc_file, tok_lines, elevel,
+					   depth);
+	free_auth_file(inc_file, depth);
+	pfree(inc_fullname);
+}
+
 /*
  * tokenize_expand_file
  *		Expand a file included from another file into an hba "field"
@@ -471,7 +529,8 @@ next_field_expand(const char *filename, char **lineptr,
  * and returns all values found therein as a flat list of AuthTokens.  If a
  * @-token is found, recursively expand it.  The newly read tokens are
  * appended to "tokens" (so that foo,bar,@baz does what you expect).
- * All new tokens are allocated in caller's memory context.
+ * All new tokens are allocated in the memory context dedicated to the
+ * tokenization.
  *
  * In event of an error, log a message at ereport level elevel, and also
  * set *err_msg to a string describing the error.  Note that the result
@@ -488,7 +547,7 @@ tokenize_expand_file(List *tokens,
 {
 	char	   *inc_fullname;
 	FILE	   *inc_file;
-	List	   *inc_lines;
+	List	   *inc_lines = NIL;
 	ListCell   *inc_line;
 
 	inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename);
@@ -705,6 +764,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 
 	while (!feof(file) && !ferror(file))
 	{
+		TokenizedAuthLine *tok_line;
 		char	   *lineptr;
 		List	   *current_line = NIL;
 		char	   *err_msg = NULL;
@@ -773,23 +833,115 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 		}
 
 		/*
-		 * Reached EOL; emit line to TokenizedAuthLine list unless it's boring
+		 * Reached EOL; no need to emit line to TokenizedAuthLine list if it's
+		 * boring.
 		 */
-		if (current_line != NIL || err_msg != NULL)
-		{
-			TokenizedAuthLine *tok_line;
+		if (current_line == NIL && err_msg == NULL)
+			goto next_line;
 
-			oldcxt = MemoryContextSwitchTo(tokenize_context);
-			tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine));
-			tok_line->fields = current_line;
-			tok_line->file_name = pstrdup(filename);
-			tok_line->line_num = line_number;
-			tok_line->raw_line = pstrdup(buf.data);
-			tok_line->err_msg = err_msg ? pstrdup(err_msg) : NULL;
-			*tok_lines = lappend(*tok_lines, tok_line);
-			MemoryContextSwitchTo(oldcxt);
+		/* If the line is valid, check if that's an include directive */
+		if (err_msg == NULL && list_length(current_line) == 2)
+		{
+			AuthToken  *first,
+					   *second;
+
+			first = linitial(linitial_node(List, current_line));
+			second = linitial(lsecond_node(List, current_line));
+
+			if (strcmp(first->string, "include") == 0)
+			{
+				tokenize_inclusion_file(filename, second->string, tok_lines,
+										elevel, depth + 1, false, &err_msg);
+
+				if (err_msg)
+					goto process_line;
+
+				/*
+				 * tokenize_auth_file() has taken care of creating the
+				 * TokenizedAuthLines.
+				 */
+				goto next_line;
+			}
+			else if (strcmp(first->string, "include_dir") == 0)
+			{
+				char	  **filenames;
+				char	   *dir_name = second->string;
+				int			num_filenames;
+				StringInfoData err_buf;
+
+				filenames = GetConfFilesInDir(dir_name, filename, elevel,
+											  &num_filenames, &err_msg);
+
+				if (!filenames)
+				{
+					/* the error is in err_msg, so create an entry */
+					goto process_line;
+				}
+
+				initStringInfo(&err_buf);
+				for (int i = 0; i < num_filenames; i++)
+				{
+					tokenize_inclusion_file(filename, filenames[i], tok_lines,
+											elevel, depth + 1, false, &err_msg);
+					/* cumulate errors if any */
+					if (err_msg)
+					{
+						if (err_buf.len > 0)
+							appendStringInfoChar(&err_buf, '\n');
+						appendStringInfoString(&err_buf, err_msg);
+					}
+				}
+
+				/* clean up things */
+				for (int i = 0; i < num_filenames; i++)
+					pfree(filenames[i]);
+				pfree(filenames);
+
+				/*
+				 * If there were no errors, the line is fully processed,
+				 * bypass the general TokenizedAuthLine processing.
+				 */
+				if (err_buf.len == 0)
+					goto next_line;
+
+				/* Otherwise, process the cumulated errors, if any. */
+				err_msg = err_buf.data;
+				goto process_line;
+			}
+			else if (strcmp(first->string, "include_if_exists") == 0)
+			{
+
+				tokenize_inclusion_file(filename, second->string, tok_lines,
+										elevel, depth + 1, true, &err_msg);
+				if (err_msg)
+					goto process_line;
+
+				/*
+				 * tokenize_auth_file() has taken care of creating the
+				 * TokenizedAuthLines.
+				 */
+				goto next_line;
+			}
 		}
 
+process_line:
+
+		/*
+		 * General processing: report the error if any and emit line to the
+		 * TokenizedAuthLine.  This is saved in the memory context dedicated
+		 * to this list.
+		 */
+		oldcxt = MemoryContextSwitchTo(tokenize_context);
+		tok_line = (TokenizedAuthLine *) palloc0(sizeof(TokenizedAuthLine));
+		tok_line->fields = current_line;
+		tok_line->file_name = pstrdup(filename);
+		tok_line->line_num = line_number;
+		tok_line->raw_line = pstrdup(buf.data);
+		tok_line->err_msg = err_msg ? pstrdup(err_msg) : NULL;
+		*tok_lines = lappend(*tok_lines, tok_line);
+		MemoryContextSwitchTo(oldcxt);
+
+next_line:
 		line_number += continuations + 1;
 		callback_arg.linenum = line_number;
 	}
diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample
index 5f3f63eb0c..f72f471d23 100644
--- a/src/backend/libpq/pg_hba.conf.sample
+++ b/src/backend/libpq/pg_hba.conf.sample
@@ -5,6 +5,10 @@
 # documentation for a complete description of this file.  A short
 # synopsis follows.
 #
+# -------------------------------
+# Authentication records
+# -------------------------------
+#
 # This file controls: which hosts are allowed to connect, how clients
 # are authenticated, which PostgreSQL user names they can use, which
 # databases they can access.  Records take one of these forms:
@@ -64,11 +68,34 @@
 # its special character, and just match a database or username with
 # that name.
 #
+# --------------------------------
+# Inclusion records
+# ---------------------------------
+#
+# This files allow the inclusion of external files or directories holding
+# extra authentication records, using the following keywords:
+#
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+#
+# FILE is the file name to include, and DIR is the directory name containing
+# the file(s) to include.  Any file in a directory will be loaded if suffixed
+# with ".conf".  The files of a directory are ordered by name.
+# include_if_exists ignored missing files.  FILE and DIRECTORY can be
+# specified as a relative or absolute path, and can be double-quoted if they
+# contain spaces.
+#
+# -------------------------------
+# Miscellaneous
+# -------------------------------
+#
 # This file is read on server startup and when the server receives a
 # SIGHUP signal.  If you edit the file on a running system, you have to
 # SIGHUP the server for the changes to take effect, run "pg_ctl reload",
 # or execute "SELECT pg_reload_conf()".
 #
+# ---------------------------------
 # Put your actual configuration here
 # ----------------------------------
 #
diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample
index a5870e6448..59092bacf2 100644
--- a/src/backend/libpq/pg_ident.conf.sample
+++ b/src/backend/libpq/pg_ident.conf.sample
@@ -1,6 +1,10 @@
 # PostgreSQL User Name Maps
 # =========================
 #
+# ------------------------------
+# Ident Records
+# ------------------------------
+#
 # Refer to the PostgreSQL documentation, chapter "Client
 # Authentication" for a complete description.  A short synopsis
 # follows.
@@ -13,6 +17,14 @@
 #
 # (The uppercase quantities must be replaced by actual values.)
 #
+# If the first field is "include", "include_if_exists" or "include_dir", it's
+# not a mapping record but a directive to include records from respectively
+# another file, another file if it exists or all the files in the given
+# directory ending in '.conf'.  FILE is the file name to include, and
+# DIR is the directory name containing the file(s) to include. FILE and
+# DIRECTORY can be specified with a relative or absolute path, and can be
+# double quoted if they contains spaces.
+#
 # MAPNAME is the (otherwise freely chosen) map name that was used in
 # pg_hba.conf.  SYSTEM-USERNAME is the detected user name of the
 # client.  PG-USERNAME is the requested PostgreSQL user name.  The
@@ -31,6 +43,28 @@
 # system user names and PostgreSQL user names are the same, you don't
 # need anything in this file.
 #
+# ------------------------------
+# Inclusion records
+# ------------------------------
+#
+# This files allow the inclusion of external files or directories holding
+# extra records, using the following keywords:
+#
+# include           FILE
+# include_if_exists FILE
+# include_dir       DIRECTORY
+#
+# FILE is the file name to include, and DIR is the directory name containing
+# the file(s) to include.  Any file in a directory will be loaded if suffixed
+# with ".conf".  The files of a directory are ordered by name.
+# include_if_exists ignored missing files.  FILE and DIRECTORY can be
+# specified as a relative or absolute path, and can be double-quoted if they
+# contain spaces.
+#
+# -------------------------------
+# Miscellaneous
+# -------------------------------
+#
 # This file is read on server startup and when the postmaster receives
 # a SIGHUP signal.  If you edit the file on a running system, you have
 # to SIGHUP the postmaster for the changes to take effect.  You can
diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c
index 87996da487..633eda30d3 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -26,12 +26,12 @@
 
 static ArrayType *get_hba_options(HbaLine *hba);
 static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-						  int rule_number, int lineno, HbaLine *hba,
-						  const char *err_msg);
+						  int rule_number, char *filename, int lineno,
+						  HbaLine *hba, const char *err_msg);
 static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-							int map_number, int lineno, IdentLine *ident,
-							const char *err_msg);
+							int map_number, char *filename, int lineno,
+							IdentLine *ident, const char *err_msg);
 static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc);
 
 
@@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba)
 }
 
 /* Number of columns in pg_hba_file_rules view */
-#define NUM_PG_HBA_FILE_RULES_ATTS	 10
+#define NUM_PG_HBA_FILE_RULES_ATTS	 11
 
 /*
  * fill_hba_line
@@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * rule_number: unique identifier among all valid rules
- * lineno: pg_hba.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * hba: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba)
  */
 static void
 fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-			  int rule_number, int lineno, HbaLine *hba,
+			  int rule_number, char *filename, int lineno, HbaLine *hba,
 			  const char *err_msg)
 {
 	Datum		values[NUM_PG_HBA_FILE_RULES_ATTS];
@@ -203,6 +204,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(rule_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -346,7 +350,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -402,7 +406,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			rule_number++;
 
 		fill_hba_line(tuple_store, tupdesc, rule_number,
-					  tok_line->line_num, hbaline, tok_line->err_msg);
+					  tok_line->file_name, tok_line->line_num, hbaline,
+					  tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
@@ -439,7 +444,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
 }
 
 /* Number of columns in pg_ident_file_mappings view */
-#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 6
+#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS	 7
 
 /*
  * fill_ident_line: build one row of pg_ident_file_mappings view, add it to
@@ -448,7 +453,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  * tuple_store: where to store data
  * tupdesc: tuple descriptor for the view
  * map_number: unique identifier among all valid maps
- * lineno: pg_ident.conf line number (must always be valid)
+ * filename: configuration file name (must always be valid)
+ * lineno: line number of configuration file (must always be valid)
  * ident: parsed line data (can be NULL, in which case err_msg should be set)
  * err_msg: error message (NULL if none)
  *
@@ -457,7 +463,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS)
  */
 static void
 fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
-				int map_number, int lineno, IdentLine *ident,
+				int map_number, char *filename, int lineno, IdentLine *ident,
 				const char *err_msg)
 {
 	Datum		values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS];
@@ -477,6 +483,9 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 		values[index++] = Int32GetDatum(map_number);
 
+	/* file_name */
+	values[index++] = CStringGetTextDatum(filename);
+
 	/* line_number */
 	values[index++] = Int32GetDatum(lineno);
 
@@ -489,7 +498,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc,
 	else
 	{
 		/* no parsing result, so set relevant fields to nulls */
-		memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool));
+		memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool));
 	}
 
 	/* error */
@@ -544,8 +553,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc)
 			map_number++;
 
 		fill_ident_line(tuple_store, tupdesc, map_number,
-						tok_line->line_num, identline,
-						tok_line->err_msg);
+						tok_line->file_name, tok_line->line_num,
+						identline, tok_line->err_msg);
 	}
 
 	/* Free tokenizer memory */
diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index c2b48c43c9..cfc23fa213 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -7,6 +7,7 @@ tests += {
       't/001_password.pl',
       't/002_saslprep.pl',
       't/003_peer.pl',
+      't/004_file_inclusion.pl',
     ],
   },
 }
diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
new file mode 100644
index 0000000000..dbcdb3320f
--- /dev/null
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -0,0 +1,721 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run    qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file   = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g;    # reduce /./ to /
+$data_dir =~ s/\/\//\//g;      # reduce // to /
+$data_dir =~ s/\/$//;          # remove trailing /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [ $filename, $fileline, $err_str ];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [ $filename, $fileline, $err_str ];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens    = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}';    # database
+	$tokens[2] = '{' . $tokens[2] . '}';    # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [ $filename, $fileline, $err_str ];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [ $filename, $fileline, $err_str ];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node           = shift;
+	my $hba_expected   = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres',
+		"ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres',
+		"ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf',   "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .=
+	  add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf',
+		"include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf',
+		"local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf',
+		"ignore - should not be included");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n"
+	  . ($cur_line{'hba_rule'} - 1)
+	  . "|$data_dir/$hba_file|"
+	  . ($cur_line{$hba_file} - 1)
+	  . '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .=
+	  add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file",
+		"include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf',
+		"include pg_ident_pos2.conf");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file",
+		"include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file",
+		"include_if_exists ../ident_inc_if/some");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf',
+		"ignore - should not be included");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str  = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if (    $err_str !~ /maximum nesting depth exceeded/
+			and $err_str !~ /could not open file/)
+		{
+			push @errors,
+			  qr/line $fileline of configuration file "$data_dir\/$filename"/;
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str  = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str";
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+	  PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents, $_, "Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+		  PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql('postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0), 'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql('postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba), 'pg_hba_file_rules content is expected');
+
+is($node->safe_psql('postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident), 'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like(
+	$node,
+	$hba_file,
+	"include ../not_a_file",
+	[
+		qr/could not open file "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	],
+	1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like(
+	$node,
+	$hba_file,
+	"include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	],
+	1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1,
+  add_hba_line(
+	$node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file,
+	"include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node,
+	"hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open file \"$data_dir/dbnames_fails.conf\": No such file or directory"
+  );
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node,
+	"recurse.conf",
+	"include recurse.conf",
+	"could not open file \"$data_dir/recurse.conf\": maximum nesting depth exceeded"
+  );
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors =
+  generate_log_err_patterns($node, \@hba_raw_errors_step2, 1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+  qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like($node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1,
+  add_ident_line(
+	$node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file,
+	"include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0), 0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2,
+  add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2,
+  add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2,
+  add_ident_line(
+	$node,
+	"ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open file \"$data_dir/osnames_fails.conf\": No such file or directory"
+  );
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2,
+  add_ident_line(
+	$node,
+	"ident_recurse.conf",
+	"include ident_recurse.conf",
+	"could not open file \"$data_dir/ident_recurse.conf\": maximum nesting depth exceeded"
+  );
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is( $node->safe_psql(
+		'postgres',
+		'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is( $node->safe_psql(
+		'postgres',
+		'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'
+	),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors,
+  add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory"
+  );
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql(
+	'postgres', 'SELECT 1',
+	stdout => \$stdout,
+	stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like(
+		$stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag(
+		"Build not compatible with auth file view error reporting, bail out.\n"
+	);
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is( $node->safe_psql(
+		'postgres',
+		'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+		  . ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors,
+  add_ident_line(
+	$node,
+	$ident_file,
+	"include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory"
+  );
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is( $node->safe_psql(
+		'postgres',
+		'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+		  . ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 7c7adbc004..37c1c86473 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1339,6 +1339,7 @@ pg_group| SELECT pg_authid.rolname AS groname,
    FROM pg_authid
   WHERE (NOT pg_authid.rolcanlogin);
 pg_hba_file_rules| SELECT a.rule_number,
+    a.file_name,
     a.line_number,
     a.type,
     a.database,
@@ -1348,14 +1349,15 @@ pg_hba_file_rules| SELECT a.rule_number,
     a.auth_method,
     a.options,
     a.error
-   FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error);
+   FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error);
 pg_ident_file_mappings| SELECT a.map_number,
+    a.file_name,
     a.line_number,
     a.map_name,
     a.sys_name,
     a.pg_username,
     a.error
-   FROM pg_ident_file_mappings() a(map_number, line_number, map_name, sys_name, pg_username, error);
+   FROM pg_ident_file_mappings() a(map_number, file_name, line_number, map_name, sys_name, pg_username, error);
 pg_indexes| SELECT n.nspname AS schemaname,
     c.relname AS tablename,
     i.relname AS indexname,
diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index 32d5d45863..e5815a5390 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -89,8 +89,8 @@
   </para>
 
   <para>
-   Each record specifies a connection type, a client IP address range
-   (if relevant for the connection type), a database name, a user name,
+   Each authentication record specifies a connection type, a client IP address
+   range (if relevant for the connection type), a database name, a user name,
    and the authentication method to be used for connections matching
    these parameters. The first record with a matching connection type,
    client address, requested database, and user name is used to perform
@@ -100,20 +100,38 @@
    access is denied.
   </para>
 
+  <para>
+   Each record can be an inclusion directive or an authentication record.
+   Inclusion directives specify files that can be included, that contain
+   additional records.  The records will be inserted in place of the
+   inclusion records.  Those records only contains two fields:
+   <literal>include</literal>, <literal>include_if_exists</literal> or
+   <literal>include_dir</literal> directive and the file or directory to be
+   included.  The file or directory can be a relative of absolute path, and can
+   be double-quoted.  For the <literal>include_dir</literal> form, all files
+   not starting with a <literal>.</literal> and ending with
+   <literal>.conf</literal> will be included.  Multiple files within an include
+   directory are processed in file name order (according to C locale rules,
+   i.e., numbers before letters, and uppercase letters before lowercase ones).
+  </para>
+
   <para>
    A record can have several formats:
 <synopsis>
-local         <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-host          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostssl       <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnossl     <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostgssenc    <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
-hostnogssenc  <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+local               <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>auth-method</replaceable> <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>address</replaceable>     <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+host                <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostssl             <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnossl           <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostgssenc          <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+hostnogssenc        <replaceable>database</replaceable>  <replaceable>user</replaceable>  <replaceable>IP-address</replaceable>  <replaceable>IP-mask</replaceable>      <replaceable>auth-method</replaceable>  <optional><replaceable>auth-options</replaceable></optional>
+include             <replaceable>file</replaceable>
+include_if_exists   <replaceable>file</replaceable>
+include_dir         <replaceable>directory</replaceable>
 </synopsis>
    The meaning of the fields is as follows:
 
@@ -655,6 +673,39 @@ openssl x509 -in myclient.crt -noout --subject -nameopt RFC2253 | sed "s/^subjec
       </para>
      </listitem>
     </varlistentry>
+
+    <varlistentry>
+     <term><literal>include</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced by the contents of the given file.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_if_exists</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of the given file if the
+       file exists and can be read.  Otherwise, a message will be logged to
+       indicate that the file is skipped.
+      </para>
+     </listitem>
+    </varlistentry>
+
+    <varlistentry>
+     <term><literal>include_dir</literal></term>
+     <listitem>
+      <para>
+       This line will be replaced with the content of all the files found in
+       the directory, if they don't start with a <literal>.</literal> and end
+       with <literal>.conf</literal>, processed in file name order (according
+       to C locale rules, i.e., numbers before letters, and uppercase letters
+       before lowercase ones).
+      </para>
+     </listitem>
+    </varlistentry>
    </variablelist>
   </para>
 
@@ -863,9 +914,11 @@ local   db1,db2,@demodbs  all                                   md5
    cluster's data directory.  (It is possible to place the map file
    elsewhere, however; see the <xref linkend="guc-ident-file"/>
    configuration parameter.)
-   The ident map file contains lines of the general form:
+   The ident map file contains lines of two general form:
 <synopsis>
 <replaceable>map-name</replaceable> <replaceable>system-username</replaceable> <replaceable>database-username</replaceable>
+<replaceable>include</replaceable> <replaceable>file</replaceable>
+<replaceable>include_dir</replaceable> <replaceable>directory</replaceable>
 </synopsis>
    Comments, whitespace and line continuations are handled in the same way as in
    <filename>pg_hba.conf</filename>.  The
@@ -875,6 +928,10 @@ local   db1,db2,@demodbs  all                                   md5
    database user name. The same <replaceable>map-name</replaceable> can be
    used repeatedly to specify multiple user-mappings within a single map.
   </para>
+  <para>
+   As for <filename>pg_hba.conf</filename>, the lines in this file can
+   be inclusion directives, following the same rules.
+  </para>
   <para>
    There is no restriction regarding how many database users a given
    operating system user can correspond to, nor vice versa.  Thus, entries
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 7c716fe327..d38b42c5cd 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -1002,12 +1002,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this rule
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this rule in <filename>pg_hba.conf</filename>
+       Line number of this rule in <literal>file_name</literal>
       </para></entry>
      </row>
 
@@ -1152,12 +1161,21 @@
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>file_name</structfield> <type>text</type>
+      </para>
+      <para>
+       Name of the file containing this map
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>line_number</structfield> <type>int4</type>
       </para>
       <para>
-       Line number of this map in <filename>pg_ident.conf</filename>
+       Line number of this map in <literal>file_name</literal>
       </para></entry>
      </row>
 
-- 
2.38.1

#84Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#83)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

Sorry for the very late answer, I had quite a lot of other things going on
recently. And thanks for taking care of the patchset!

On Wed, Nov 23, 2022 at 03:05:18PM +0900, Michael Paquier wrote:

On Tue, Nov 22, 2022 at 05:20:01PM +0900, Michael Paquier wrote:

+     /* XXX: this should stick to elevel for some cases? */
+     ereport(LOG,
+             (errmsg("skipping missing authentication file \"%s\"",
+                     inc_fullname)));
Should we always issue a LOG here?  In some cases we use an elevel of
DEBUG3.

And here I think that we need to use elevel. In hba.c, this would
generate a LOG while hbafuncs.c uses DEBUG3, leading to less noise in
the log files.

Yeah I agree the LOG message is only interesting if you're reloading the conf.
I actually thought that this is what I did sorry about that.

So, what do you think about something like the attached? I have begun
a lookup at the tests, but I don't have enough material for an actual
review of this part yet. Note that I have removed temporarily
process_included_authfile(), as I was looking at all the code branches
in details. The final result ought to include AbsoluteConfigLocation(),
open_auth_file() and tokenize_auth_file() with an extra missing_ok
argument, I guess ("strict" as proposed originally is not the usual
PG-way for ENOENT-ish problems). process_line should be used only
when we have no err_msg, meaning that these have been consumed in some
TokenizedAuthLines already.

This, however, was still brittle in terms of memory handling.
Reloading the server a few hundred times proved that this was leaking
memory in the tokenization.

Oh, nice catch.

This becomes quite simple once you switch
to an approach where tokenize_auth_file() uses its own memory context,
while we store all the TokenizedAuthLines in the static context. It
also occurred to me that we can create and drop the static context
holding the tokens when opening the top-root auth file, aka with a
depth at 0. It may be a bit simpler to switch to a single-context
approach for all the allocations of tokenize_auth_file(), though I
find the use of a static context much cleaner for the inclusions with
@ files when we expand an existing list.

Agreed.

The depth 0 is getting used quite a lot now, maybe we should have a define for
it to make it easier to grep, like TOP_LEVEL_AUTH_FILE or something like that?
And also add a define for the magical 10 for the max inclusion depth, for both
auth files and GUC files while at it?

The patch can be split, again, into more pieces. Attached 0001
reworks the memory allocation contexts for the tokenization and 0002
is the main feature. As of things are, I am quite happy with the
shapes of 0001 and 0002. I have tested quite a bit its robustness,
with valgrind for example, to make sure that there are no leaks in the
postmaster (with[out] -DEXEC_BACKEND). The inclusion logic is
refactored to be in a state close to your previous patches, see
tokenize_inclusion_file().

Yep I saw that. I don't have much to say about the patch, it looks good to me.
The only nitpicking I have is:

+/*
+ * Memory context holding the list of TokenizedAuthLines when parsing
+ * HBA or ident config files.  This is created when loading the top HBA
+ + or ident file (depth of 0).
+ */
+static MemoryContext tokenize_context = NULL;

The comment seems a bit ambiguous as with "loading the top..." you probably
meant something like "loading the file in memory" rather than "(re)loading the
configuration". Maybe s/loading/opening/?

Note that the tests are failing for some of the Windows CIs, actually,
due to a difference in some of the paths generated vs the file paths
(aka c:\cirrus vs c:/cirrus, as far as I saw).

Mmm, I haven't looked deeply so I'm not sure if the perl podules are aware of
it or not, but maybe we could somehow detect the used delimiter at the
beginning after normalizing the directory, and use a $DELIM rather than a plain
"/"?

#85Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#84)
Re: Allow file inclusion in pg_hba and pg_ident files

On Wed, Nov 23, 2022 at 03:56:50PM +0800, Julien Rouhaud wrote:

The depth 0 is getting used quite a lot now, maybe we should have a define for
it to make it easier to grep, like TOP_LEVEL_AUTH_FILE or something like that?
And also add a define for the magical 10 for the max inclusion depth, for both
auth files and GUC files while at it?

Sounds like a good idea to me, and it seems to me that this had better
be unified between the GUCs (see ParseConfigFp() that hardcodes a
depth of 0) and hba.c. It looks like they could be added to
conffiles.h, as of CONF_FILE_START_{LEVEL,DEPTH} and
CONF_FILE_MAX_{LEVEL,DEPTH}. Would you like to send a patch?

The comment seems a bit ambiguous as with "loading the top..." you probably
meant something like "loading the file in memory" rather than "(re)loading the
configuration". Maybe s/loading/opening/?

Right. I have used "opening" at the end.

I have worked on all that, did more polishing to the docs, the
comments and some tiny bits of the logic, and applied both 0001 and
0002. Now, to the tests..

Mmm, I haven't looked deeply so I'm not sure if the perl podules are aware of
it or not, but maybe we could somehow detect the used delimiter at the
beginning after normalizing the directory, and use a $DELIM rather than a plain
"/"?

I am not sure. Could you have a look and see if you can get the CI
back to green? The first thing I would test is to switch the error
patterns to be regexps based on the basenames rather than the full
paths (tweaking the queries on the system views to do htat), so as we
avoid all this business with slash and backslash transformations.
--
Michael

#86Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#85)
Re: Allow file inclusion in pg_hba and pg_ident files

On Thu, Nov 24, 2022 at 02:07:21PM +0900, Michael Paquier wrote:

On Wed, Nov 23, 2022 at 03:56:50PM +0800, Julien Rouhaud wrote:

The depth 0 is getting used quite a lot now, maybe we should have a define for
it to make it easier to grep, like TOP_LEVEL_AUTH_FILE or something like that?
And also add a define for the magical 10 for the max inclusion depth, for both
auth files and GUC files while at it?

Sounds like a good idea to me, and it seems to me that this had better
be unified between the GUCs (see ParseConfigFp() that hardcodes a
depth of 0) and hba.c. It looks like they could be added to
conffiles.h, as of CONF_FILE_START_{LEVEL,DEPTH} and
CONF_FILE_MAX_{LEVEL,DEPTH}. Would you like to send a patch?

Agreed, and yes I will take care of that shortly.

The comment seems a bit ambiguous as with "loading the top..." you probably
meant something like "loading the file in memory" rather than "(re)loading the
configuration". Maybe s/loading/opening/?

Right. I have used "opening" at the end.

I have worked on all that, did more polishing to the docs, the
comments and some tiny bits of the logic, and applied both 0001 and
0002.

Thanks a lot!

Now, to the tests..

Mmm, I haven't looked deeply so I'm not sure if the perl podules are aware of
it or not, but maybe we could somehow detect the used delimiter at the
beginning after normalizing the directory, and use a $DELIM rather than a plain
"/"?

I am not sure. Could you have a look and see if you can get the CI
back to green? The first thing I would test is to switch the error
patterns to be regexps based on the basenames rather than the full
paths (tweaking the queries on the system views to do htat), so as we
avoid all this business with slash and backslash transformations.

I'm also working on it! Hopefully I should be able to come up with a fix soon.

#87Julien Rouhaud
rjuju123@gmail.com
In reply to: Julien Rouhaud (#86)
2 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

Hi,

On Thu, Nov 24, 2022 at 02:37:23PM +0800, Julien Rouhaud wrote:

On Thu, Nov 24, 2022 at 02:07:21PM +0900, Michael Paquier wrote:

On Wed, Nov 23, 2022 at 03:56:50PM +0800, Julien Rouhaud wrote:

The depth 0 is getting used quite a lot now, maybe we should have a define for
it to make it easier to grep, like TOP_LEVEL_AUTH_FILE or something like that?
And also add a define for the magical 10 for the max inclusion depth, for both
auth files and GUC files while at it?

Sounds like a good idea to me, and it seems to me that this had better
be unified between the GUCs (see ParseConfigFp() that hardcodes a
depth of 0) and hba.c. It looks like they could be added to
conffiles.h, as of CONF_FILE_START_{LEVEL,DEPTH} and
CONF_FILE_MAX_{LEVEL,DEPTH}. Would you like to send a patch?

So I went with CONF_FILE_START_DEPTH and CONF_FILE_MAX_DEPTH. Attached v22
that fixes it in all the places I found.

Now, to the tests..

Mmm, I haven't looked deeply so I'm not sure if the perl podules are aware of
it or not, but maybe we could somehow detect the used delimiter at the
beginning after normalizing the directory, and use a $DELIM rather than a plain
"/"?

I am not sure. Could you have a look and see if you can get the CI
back to green? The first thing I would test is to switch the error
patterns to be regexps based on the basenames rather than the full
paths (tweaking the queries on the system views to do htat), so as we
avoid all this business with slash and backslash transformations.

Apparently just making sure that the $node->data_dir consistently uses forward
slashes is enough to make the CI happy, for VS 2019 [1]https://cirrus-ci.com/task/4944203946917888 and MinGW64 [2]https://cirrus-ci.com/task/6070103853760512, so
done this way with an extra normalization step.

[1]: https://cirrus-ci.com/task/4944203946917888
[2]: https://cirrus-ci.com/task/6070103853760512

Attachments:

v22-0001-Introduce-macros-for-initial-maximum-depth-level.patchtext/plain; charset=us-asciiDownload
From 879cf469d00d9274b67b80eb5fe47dfccf03022d Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Thu, 24 Nov 2022 16:57:53 +0800
Subject: [PATCH v22 1/2] Introduce macros for initial/maximum depth levels for
 configuration files

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya@jrouhaud
---
 src/backend/commands/extension.c  | 4 +++-
 src/backend/libpq/hba.c           | 8 ++++----
 src/backend/utils/misc/guc-file.l | 5 +++--
 src/backend/utils/misc/guc.c      | 8 +++++---
 src/include/utils/conffiles.h     | 3 +++
 5 files changed, 18 insertions(+), 10 deletions(-)

diff --git a/src/backend/commands/extension.c b/src/backend/commands/extension.c
index 806d6056ab..de01b792b9 100644
--- a/src/backend/commands/extension.c
+++ b/src/backend/commands/extension.c
@@ -60,6 +60,7 @@
 #include "tcop/utility.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
+#include "utils/conffiles.h"
 #include "utils/fmgroids.h"
 #include "utils/lsyscache.h"
 #include "utils/memutils.h"
@@ -515,7 +516,8 @@ parse_extension_control_file(ExtensionControlFile *control,
 	 * Parse the file content, using GUC's file parsing code.  We need not
 	 * check the return value since any errors will be thrown at ERROR level.
 	 */
-	(void) ParseConfigFp(file, filename, 0, ERROR, &head, &tail);
+	(void) ParseConfigFp(file, filename, CONF_FILE_START_DEPTH, ERROR, &head,
+						 &tail);
 
 	FreeFile(file);
 
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 862ec18e91..8f1a0c4c73 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -620,7 +620,7 @@ free_auth_file(FILE *file, int depth)
 	FreeFile(file);
 
 	/* If this is the last cleanup, remove the tokenization context */
-	if (depth == 0)
+	if (depth == CONF_FILE_START_DEPTH)
 	{
 		MemoryContextDelete(tokenize_context);
 		tokenize_context = NULL;
@@ -650,7 +650,7 @@ open_auth_file(const char *filename, int elevel, int depth,
 	 * avoid dumping core due to stack overflow if an include file loops back
 	 * to itself.  The maximum nesting depth is pretty arbitrary.
 	 */
-	if (depth > 10)
+	if (depth > CONF_FILE_MAX_DEPTH)
 	{
 		ereport(elevel,
 				(errcode_for_file_access(),
@@ -684,7 +684,7 @@ open_auth_file(const char *filename, int elevel, int depth,
 	 * tokenization.  This will be closed with this file when coming back to
 	 * this level of cleanup.
 	 */
-	if (depth == 0)
+	if (depth == CONF_FILE_START_DEPTH)
 	{
 		/*
 		 * A context may be present, but assume that it has been eliminated
@@ -762,7 +762,7 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines,
 
 	initStringInfo(&buf);
 
-	if (depth == 0)
+	if (depth == CONF_FILE_START_DEPTH)
 		*tok_lines = NIL;
 
 	while (!feof(file) && !ferror(file))
diff --git a/src/backend/utils/misc/guc-file.l b/src/backend/utils/misc/guc-file.l
index 88245475d1..f7e4457ded 100644
--- a/src/backend/utils/misc/guc-file.l
+++ b/src/backend/utils/misc/guc-file.l
@@ -202,7 +202,7 @@ ParseConfigFile(const char *config_file, bool strict,
 	 * avoid dumping core due to stack overflow if an include file loops back
 	 * to itself.  The maximum nesting depth is pretty arbitrary.
 	 */
-	if (depth > 10)
+	if (depth > CONF_FILE_MAX_DEPTH)
 	{
 		ereport(elevel,
 				(errcode(ERRCODE_PROGRAM_LIMIT_EXCEEDED),
@@ -321,7 +321,8 @@ GUC_flex_fatal(const char *msg)
  * Input parameters:
  *	fp: file pointer from AllocateFile for the configuration file to parse
  *	config_file: absolute or relative path name of the configuration file
- *	depth: recursion depth (should be 0 in the outermost call)
+ *	depth: recursion depth (should be CONF_FILE_START_DEPTH in the outermost
+ *	call)
  *	elevel: error logging level to use
  * Input/Output parameters:
  *	head_p, tail_p: head and tail of linked list of name/value pairs
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index 117a2d26a0..28313b3a94 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -44,6 +44,7 @@
 #include "utils/acl.h"
 #include "utils/backend_status.h"
 #include "utils/builtins.h"
+#include "utils/conffiles.h"
 #include "utils/float.h"
 #include "utils/guc_tables.h"
 #include "utils/memutils.h"
@@ -287,7 +288,7 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel)
 	head = tail = NULL;
 
 	if (!ParseConfigFile(ConfigFileName, true,
-						 NULL, 0, 0, elevel,
+						 NULL, 0, CONF_FILE_START_DEPTH, elevel,
 						 &head, &tail))
 	{
 		/* Syntax error(s) detected in the file, so bail out */
@@ -304,7 +305,7 @@ ProcessConfigFileInternal(GucContext context, bool applySettings, int elevel)
 	if (DataDir)
 	{
 		if (!ParseConfigFile(PG_AUTOCONF_FILENAME, false,
-							 NULL, 0, 0, elevel,
+							 NULL, 0, CONF_FILE_START_DEPTH, elevel,
 							 &head, &tail))
 		{
 			/* Syntax error(s) detected in the file, so bail out */
@@ -4582,7 +4583,8 @@ AlterSystemSetConfigFile(AlterSystemStmt *altersysstmt)
 								AutoConfFileName)));
 
 			/* parse it */
-			if (!ParseConfigFp(infile, AutoConfFileName, 0, LOG, &head, &tail))
+			if (!ParseConfigFp(infile, AutoConfFileName, CONF_FILE_START_DEPTH,
+							   LOG, &head, &tail))
 				ereport(ERROR,
 						(errcode(ERRCODE_CONFIG_FILE_ERROR),
 						 errmsg("could not parse contents of file \"%s\"",
diff --git a/src/include/utils/conffiles.h b/src/include/utils/conffiles.h
index 3f23a2a011..294a88c854 100644
--- a/src/include/utils/conffiles.h
+++ b/src/include/utils/conffiles.h
@@ -13,6 +13,9 @@
 #ifndef CONFFILES_H
 #define CONFFILES_H
 
+#define CONF_FILE_START_DEPTH		0
+#define CONF_FILE_MAX_DEPTH			10
+
 extern char *AbsoluteConfigLocation(const char *location,
 									const char *calling_file);
 extern char **GetConfFilesInDir(const char *includedir,
-- 
2.37.0

v22-0002-Add-regression-tests-for-file-inclusion-in-HBA-e.patchtext/plain; charset=us-asciiDownload
From 5f7c7cc9a77cced1851a65758a7befde80d39f3e Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Thu, 24 Nov 2022 14:20:22 +0800
Subject: [PATCH v22 2/2] Add regression tests for file inclusion in HBA end
 ident configuration files

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya@jrouhaud
---
 src/test/authentication/meson.build           |   1 +
 .../authentication/t/004_file_inclusion.pl    | 722 ++++++++++++++++++
 2 files changed, 723 insertions(+)
 create mode 100644 src/test/authentication/t/004_file_inclusion.pl

diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index c2b48c43c9..cfc23fa213 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -7,6 +7,7 @@ tests += {
       't/001_password.pl',
       't/002_saslprep.pl',
       't/003_peer.pl',
+      't/004_file_inclusion.pl',
     ],
   },
 }
diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
new file mode 100644
index 0000000000..1be807c0a2
--- /dev/null
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -0,0 +1,722 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run    qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file   = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g;    # reduce /./ to /
+$data_dir =~ s/\/\//\//g;      # reduce // to /
+$data_dir =~ s/\/$//;          # remove trailing /
+$data_dir =~ s/\\/\//g;        # change \ to /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [ $filename, $fileline, $err_str ];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [ $filename, $fileline, $err_str ];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens    = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}';    # database
+	$tokens[2] = '{' . $tokens[2] . '}';    # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [ $filename, $fileline, $err_str ];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [ $filename, $fileline, $err_str ];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node           = shift;
+	my $hba_expected   = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres',
+		"ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres',
+		"ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf',   "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .=
+	  add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf',
+		"include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf',
+		"local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf',
+		"ignore - should not be included");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n"
+	  . ($cur_line{'hba_rule'} - 1)
+	  . "|$data_dir/$hba_file|"
+	  . ($cur_line{$hba_file} - 1)
+	  . '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .=
+	  add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file",
+		"include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf',
+		"include pg_ident_pos2.conf");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file",
+		"include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file",
+		"include_if_exists ../ident_inc_if/some");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf',
+		"ignore - should not be included");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str  = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if (    $err_str !~ /maximum nesting depth exceeded/
+			and $err_str !~ /could not open file/)
+		{
+			push @errors,
+			  qr/line $fileline of configuration file "$data_dir\/$filename"/;
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str  = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str";
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+	  PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents, $_, "Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+		  PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql('postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0), 'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql('postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba), 'pg_hba_file_rules content is expected');
+
+is($node->safe_psql('postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident), 'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like(
+	$node,
+	$hba_file,
+	"include ../not_a_file",
+	[
+		qr/could not open file "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	],
+	1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like(
+	$node,
+	$hba_file,
+	"include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	],
+	1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1,
+  add_hba_line(
+	$node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file,
+	"include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node,
+	"hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open file \"$data_dir/dbnames_fails.conf\": No such file or directory"
+  );
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node,
+	"recurse.conf",
+	"include recurse.conf",
+	"could not open file \"$data_dir/recurse.conf\": maximum nesting depth exceeded"
+  );
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors =
+  generate_log_err_patterns($node, \@hba_raw_errors_step2, 1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+  qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like($node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1,
+  add_ident_line(
+	$node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file,
+	"include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0), 0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2,
+  add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2,
+  add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2,
+  add_ident_line(
+	$node,
+	"ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open file \"$data_dir/osnames_fails.conf\": No such file or directory"
+  );
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2,
+  add_ident_line(
+	$node,
+	"ident_recurse.conf",
+	"include ident_recurse.conf",
+	"could not open file \"$data_dir/ident_recurse.conf\": maximum nesting depth exceeded"
+  );
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is( $node->safe_psql(
+		'postgres',
+		'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is( $node->safe_psql(
+		'postgres',
+		'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'
+	),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors,
+  add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory"
+  );
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql(
+	'postgres', 'SELECT 1',
+	stdout => \$stdout,
+	stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like(
+		$stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag(
+		"Build not compatible with auth file view error reporting, bail out.\n"
+	);
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is( $node->safe_psql(
+		'postgres',
+		'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+		  . ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors,
+  add_ident_line(
+	$node,
+	$ident_file,
+	"include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory"
+  );
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is( $node->safe_psql(
+		'postgres',
+		'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+		  . ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
-- 
2.37.0

#88Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#87)
Re: Allow file inclusion in pg_hba and pg_ident files

On Thu, Nov 24, 2022 at 05:07:24PM +0800, Julien Rouhaud wrote:

So I went with CONF_FILE_START_DEPTH and CONF_FILE_MAX_DEPTH. Attached v22
that fixes it in all the places I found.

Sounds fine. Added one comment, fixed one comment, and applied.
Thanks!
--
Michael

#89Julien Rouhaud
rjuju123@gmail.com
In reply to: Michael Paquier (#88)
Re: Allow file inclusion in pg_hba and pg_ident files

On Fri, Nov 25, 2022 at 07:41:59AM +0900, Michael Paquier wrote:

On Thu, Nov 24, 2022 at 05:07:24PM +0800, Julien Rouhaud wrote:

So I went with CONF_FILE_START_DEPTH and CONF_FILE_MAX_DEPTH. Attached v22
that fixes it in all the places I found.

Sounds fine. Added one comment, fixed one comment, and applied.
Thanks!

Thanks a lot!

#90Ian Lawrence Barwick
barwick@gmail.com
In reply to: Julien Rouhaud (#89)
Re: Allow file inclusion in pg_hba and pg_ident files

2022年11月25日(金) 11:25 Julien Rouhaud <rjuju123@gmail.com>:

On Fri, Nov 25, 2022 at 07:41:59AM +0900, Michael Paquier wrote:

On Thu, Nov 24, 2022 at 05:07:24PM +0800, Julien Rouhaud wrote:

So I went with CONF_FILE_START_DEPTH and CONF_FILE_MAX_DEPTH. Attached v22
that fixes it in all the places I found.

Sounds fine. Added one comment, fixed one comment, and applied.
Thanks!

Thanks a lot!

Hi

I'm trying to reconcile open CommitFest entries with their actual
status; the entry for this:

https://commitfest.postgresql.org/40/3558/

shows as "Waiting on Author", but looks like it's all been committed;
is there anything
left to do on this?

Thanks

Ian Barwick

#91Julien Rouhaud
rjuju123@gmail.com
In reply to: Ian Lawrence Barwick (#90)
Re: Allow file inclusion in pg_hba and pg_ident files

Le dim. 27 nov. 2022 à 15:31, Ian Lawrence Barwick

I'm trying to reconcile open CommitFest entries with their actual
status; the entry for this:

https://commitfest.postgresql.org/40/3558/

shows as "Waiting on Author", but looks like it's all been committed;
is there anything
left to do on this?

right the CF entry is out of date. there's still the additional tap test
that needs to be taken care of. I sent a new version recently that works on
windows CI, so I guess the correct status should now be needs review,
although the latest patchset probably doesn't apply anymore since the first
patch has been committed. I'm traveling today I'll try to send a rebased
version in the evening

Show quoted text
#92Julien Rouhaud
rjuju123@gmail.com
In reply to: Julien Rouhaud (#91)
1 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Sun, Nov 27, 2022 at 03:39:44PM +0800, Julien Rouhaud wrote:

Le dim. 27 nov. 2022 � 15:31, Ian Lawrence Barwick

I'm trying to reconcile open CommitFest entries with their actual
status; the entry for this:

https://commitfest.postgresql.org/40/3558/

shows as "Waiting on Author", but looks like it's all been committed;
is there anything
left to do on this?

right the CF entry is out of date. there's still the additional tap test
that needs to be taken care of. I sent a new version recently that works on
windows CI, so I guess the correct status should now be needs review,
although the latest patchset probably doesn't apply anymore since the first
patch has been committed. I'm traveling today I'll try to send a rebased
version in the evening

And here's the rebased patch for the TAP tests. I will switch the CF entry to
Needs Review.

Attachments:

v23-0001-Add-regression-tests-for-file-inclusion-in-HBA-e.patchtext/plain; charset=us-asciiDownload
From 8c0c6f9f477c8f1a28d475267f3bdca59c84ed86 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouhaud@free.fr>
Date: Thu, 24 Nov 2022 14:20:22 +0800
Subject: [PATCH v23] Add regression tests for file inclusion in HBA end ident
 configuration files

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya@jrouhaud
---
 src/test/authentication/meson.build           |   1 +
 .../authentication/t/004_file_inclusion.pl    | 722 ++++++++++++++++++
 2 files changed, 723 insertions(+)
 create mode 100644 src/test/authentication/t/004_file_inclusion.pl

diff --git a/src/test/authentication/meson.build b/src/test/authentication/meson.build
index c2b48c43c9..cfc23fa213 100644
--- a/src/test/authentication/meson.build
+++ b/src/test/authentication/meson.build
@@ -7,6 +7,7 @@ tests += {
       't/001_password.pl',
       't/002_saslprep.pl',
       't/003_peer.pl',
+      't/004_file_inclusion.pl',
     ],
   },
 }
diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
new file mode 100644
index 0000000000..1be807c0a2
--- /dev/null
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -0,0 +1,722 @@
+
+# Copyright (c) 2021-2022, PostgreSQL Global Development Group
+
+# Set of tests for authentication and pg_hba.conf inclusion.
+# This test can only run with Unix-domain sockets.
+
+use strict;
+use warnings;
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(usleep);
+use IPC::Run    qw(pump finish timer);
+use Data::Dumper;
+
+if (!$use_unix_sockets)
+{
+	plan skip_all =>
+	  "authentication tests cannot run without Unix-domain sockets";
+}
+
+# stores the current line counter for each file.  hba_rule and ident_rule are
+# fake file names used for the global rule number for each auth view.
+my %cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+my $hba_file   = 'subdir1/pg_hba_custom.conf';
+my $ident_file = 'subdir2/pg_ident_custom.conf';
+
+# Initialize primary node
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init;
+$node->start;
+
+my $data_dir = $node->data_dir;
+
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g;    # reduce /./ to /
+$data_dir =~ s/\/\//\//g;      # reduce // to /
+$data_dir =~ s/\/$//;          # remove trailing /
+$data_dir =~ s/\\/\//g;        # change \ to /
+
+
+# Add the given payload to the given relative HBA file of the given node.
+# This function maintains the %cur_line metadata, so it has to be called in the
+# expected inclusion evaluation order in order to keep it in sync.
+#
+# If the payload starts with "include" or "ignore", the function doesn't
+# increase the general hba rule number.
+#
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+# Otherwise it only returns the line number of the payload in the wanted file.
+# This function has to be called in the expected inclusion evaluation order to
+# keep the %cur_line information in sync.
+sub add_hba_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [ $filename, $fileline, $err_str ];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'hba_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [ $filename, $fileline, $err_str ];
+	}
+
+	# Otherwise, generate the expected pg_hba_file_rules line
+	@tokens    = split(/ /, $payload);
+	$tokens[1] = '{' . $tokens[1] . '}';    # database
+	$tokens[2] = '{' . $tokens[2] . '}';    # user_name
+
+	# add empty address and netmask betweed user_name and auth_method
+	splice @tokens, 3, 0, '';
+	splice @tokens, 3, 0, '';
+
+	# append empty options and error
+	push @tokens, '';
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Add the given payload to the given relative ident file of the given node.
+# Same as add_hba_line but for pg_ident files
+sub add_ident_line
+{
+	my $node     = shift;
+	my $filename = shift;
+	my $payload  = shift;
+	my $err_str  = shift;
+	my $globline;
+	my $fileline;
+	my @tokens;
+	my $line;
+
+	# Append the payload to the given file
+	$node->append_conf($filename, $payload);
+
+	# Get the current %cur_line counter for the file
+	if (not defined $cur_line{$filename})
+	{
+		$cur_line{$filename} = 1;
+	}
+	$fileline = $cur_line{$filename}++;
+
+	# Include directive, don't generate an underlying pg_hba_file_rules line
+	# but make sure we incremented the %cur_line counter.
+	# Also ignore line beginning with "ignore", for content of files that
+	# should not being included
+	if ($payload =~ qr/^(include|ignore)/)
+	{
+		if (defined $err_str)
+		{
+			return [ $filename, $fileline, $err_str ];
+		}
+		else
+		{
+			return $fileline;
+		}
+	}
+
+	# Get (and increment) the global rule number
+	$globline = $cur_line{'ident_rule'}++;
+
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [ $filename, $fileline, $err_str ];
+	}
+
+	# Otherwise, generate the expected pg_ident_file_mappings line
+	@tokens = split(/ /, $payload);
+
+	# append empty error
+	push @tokens, '';
+
+	# generate the expected final line
+	$line = "";
+	$line .= "\n" if ($globline > 1);
+	$line .= "$globline|$data_dir/$filename|$fileline|";
+	$line .= join('|', @tokens);
+
+	return $line;
+}
+
+# Delete pg_hba.conf from the given node, add various entries to test the
+# include infrastructure and then execute a reload to refresh it.
+sub generate_valid_auth_files
+{
+	my $node           = shift;
+	my $hba_expected   = '';
+	my $ident_expected = '';
+
+	# customise main auth file names
+	$node->safe_psql('postgres',
+		"ALTER SYSTEM SET hba_file = '$data_dir/$hba_file'");
+	$node->safe_psql('postgres',
+		"ALTER SYSTEM SET ident_file = '$data_dir/$ident_file'");
+
+	# and make original ones invalid to be sure they're not used anywhere
+	$node->append_conf('pg_hba.conf',   "some invalid line");
+	$node->append_conf('pg_ident.conf', "some invalid line");
+
+	# pg_hba stuff
+	mkdir("$data_dir/subdir1");
+	mkdir("$data_dir/hba_inc");
+	mkdir("$data_dir/hba_inc_if");
+	mkdir("$data_dir/hba_pos");
+
+	# Make sure we will still be able to connect
+	$hba_expected .= add_hba_line($node, "$hba_file", 'local all all trust');
+
+	# Add include data
+	add_hba_line($node, "$hba_file", "include ../pg_hba_pre.conf");
+	$hba_expected .=
+	  add_hba_line($node, 'pg_hba_pre.conf', "local pre all reject");
+
+	$hba_expected .= add_hba_line($node, "$hba_file", "local all all reject");
+
+	add_hba_line($node, "$hba_file", "include ../hba_pos/pg_hba_pos.conf");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_pos/pg_hba_pos.conf', "local pos all reject");
+	# include is relative to current path
+	add_hba_line($node, 'hba_pos/pg_hba_pos.conf',
+		"include pg_hba_pos2.conf");
+	$hba_expected .= add_hba_line($node, 'hba_pos/pg_hba_pos2.conf',
+		"local pos2 all reject");
+
+	# include_if_exists data
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/none");
+	add_hba_line($node, "$hba_file", "include_if_exists ../hba_inc_if/some");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_inc_if/some', "local if_some all reject");
+
+	# include_dir data
+	add_hba_line($node, "$hba_file", "include_dir ../hba_inc");
+	add_hba_line($node, 'hba_inc/garbageconf',
+		"ignore - should not be included");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_inc/01_z.conf', "local dir_z all reject");
+	$hba_expected .=
+	  add_hba_line($node, 'hba_inc/02_a.conf', "local dir_a all reject");
+
+	# secondary auth file
+	add_hba_line($node, $hba_file, 'local @../dbnames.conf all reject');
+	$node->append_conf('dbnames.conf', "db1");
+	$node->append_conf('dbnames.conf', "db3");
+	$hba_expected .= "\n"
+	  . ($cur_line{'hba_rule'} - 1)
+	  . "|$data_dir/$hba_file|"
+	  . ($cur_line{$hba_file} - 1)
+	  . '|local|{db1,db3}|{all}|||reject||';
+
+	# pg_ident stuff
+	mkdir("$data_dir/subdir2");
+	mkdir("$data_dir/ident_inc");
+	mkdir("$data_dir/ident_inc_if");
+	mkdir("$data_dir/ident_pos");
+
+	# Add include data
+	add_ident_line($node, "$ident_file", "include ../pg_ident_pre.conf");
+	$ident_expected .=
+	  add_ident_line($node, 'pg_ident_pre.conf', "pre foo bar");
+
+	$ident_expected .= add_ident_line($node, "$ident_file", "test a b");
+
+	add_ident_line($node, "$ident_file",
+		"include ../ident_pos/pg_ident_pos.conf");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_pos/pg_ident_pos.conf', "pos foo bar");
+	# include is relative to current path
+	add_ident_line($node, 'ident_pos/pg_ident_pos.conf',
+		"include pg_ident_pos2.conf");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_pos/pg_ident_pos2.conf', "pos2 foo bar");
+
+	# include_if_exists data
+	add_ident_line($node, "$ident_file",
+		"include_if_exists ../ident_inc_if/none");
+	add_ident_line($node, "$ident_file",
+		"include_if_exists ../ident_inc_if/some");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_inc_if/some', "if_some foo bar");
+
+	# include_dir data
+	add_ident_line($node, "$ident_file", "include_dir ../ident_inc");
+	add_ident_line($node, 'ident_inc/garbageconf',
+		"ignore - should not be included");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_inc/01_z.conf', "dir_z foo bar");
+	$ident_expected .=
+	  add_ident_line($node, 'ident_inc/02_a.conf', "dir_a foo bar");
+
+	$node->restart;
+	$node->connect_ok('dbname=postgres',
+		'Connection ok after generating valid auth files');
+
+	return ($hba_expected, $ident_expected);
+}
+
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%cur_line = ('hba_rule' => 1, 'ident_rule' => 1);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str  = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if (    $err_str !~ /maximum nesting depth exceeded/
+			and $err_str !~ /could not open file/)
+		{
+			push @errors,
+			  qr/line $fileline of configuration file "$data_dir\/$filename"/;
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str  = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str";
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+	  PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents, $_, "Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+		  PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+# We should be able to connect, and see an empty pg_ident.conf
+is($node->psql('postgres', 'SELECT count(*) FROM pg_ident_file_mappings'),
+	qq(0), 'pg_ident.conf is empty');
+
+############################################
+# part 1, test view reporting for valid data
+############################################
+my ($exp_hba, $exp_ident) = generate_valid_auth_files($node);
+
+$node->connect_ok('dbname=postgres', 'Connection still ok');
+
+is($node->safe_psql('postgres', 'SELECT * FROM pg_hba_file_rules'),
+	qq($exp_hba), 'pg_hba_file_rules content is expected');
+
+is($node->safe_psql('postgres', 'SELECT * FROM pg_ident_file_mappings'),
+	qq($exp_ident), 'pg_ident_file_mappings content is expected');
+
+#############################################
+# part 2, test log reporting for invalid data
+#############################################
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like(
+	$node,
+	$hba_file,
+	"include ../not_a_file",
+	[
+		qr/could not open file "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	],
+	1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like(
+	$node,
+	$hba_file,
+	"include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	],
+	1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1,
+  add_hba_line(
+	$node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file,
+	"include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node,
+	"hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open file \"$data_dir/dbnames_fails.conf\": No such file or directory"
+  );
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node,
+	"recurse.conf",
+	"include recurse.conf",
+	"could not open file \"$data_dir/recurse.conf\": maximum nesting depth exceeded"
+  );
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors =
+  generate_log_err_patterns($node, \@hba_raw_errors_step2, 1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+  qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like($node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1,
+  add_ident_line(
+	$node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file,
+	"include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0), 0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2,
+  add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2,
+  add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2,
+  add_ident_line(
+	$node,
+	"ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open file \"$data_dir/osnames_fails.conf\": No such file or directory"
+  );
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2,
+  add_ident_line(
+	$node,
+	"ident_recurse.conf",
+	"include ident_recurse.conf",
+	"could not open file \"$data_dir/ident_recurse.conf\": maximum nesting depth exceeded"
+  );
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+#####################################################
+# part 3, test reporting of various error scenario
+# NOTE: this will be bypassed -DEXEC_BACKEND or win32
+#####################################################
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is( $node->safe_psql(
+		'postgres',
+		'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is( $node->safe_psql(
+		'postgres',
+		'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'
+	),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors,
+  add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory"
+  );
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql(
+	'postgres', 'SELECT 1',
+	stdout => \$stdout,
+	stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like(
+		$stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag(
+		"Build not compatible with auth file view error reporting, bail out.\n"
+	);
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+my $hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is( $node->safe_psql(
+		'postgres',
+		'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+		  . ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors,
+  add_ident_line(
+	$node,
+	$ident_file,
+	"include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory"
+  );
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+my $ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is( $node->safe_psql(
+		'postgres',
+		'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+		  . ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
+done_testing();
-- 
2.37.0

#93Ian Lawrence Barwick
barwick@gmail.com
In reply to: Julien Rouhaud (#92)
Re: Allow file inclusion in pg_hba and pg_ident files

2022年11月27日(日) 20:04 Julien Rouhaud <rjuju123@gmail.com>:

On Sun, Nov 27, 2022 at 03:39:44PM +0800, Julien Rouhaud wrote:

Le dim. 27 nov. 2022 à 15:31, Ian Lawrence Barwick

I'm trying to reconcile open CommitFest entries with their actual
status; the entry for this:

https://commitfest.postgresql.org/40/3558/

shows as "Waiting on Author", but looks like it's all been committed;
is there anything
left to do on this?

right the CF entry is out of date. there's still the additional tap test
that needs to be taken care of. I sent a new version recently that works on
windows CI, so I guess the correct status should now be needs review,
although the latest patchset probably doesn't apply anymore since the first
patch has been committed. I'm traveling today I'll try to send a rebased
version in the evening

And here's the rebased patch for the TAP tests. I will switch the CF entry to
Needs Review.

Thanks for the quick update!

Regards

Ian Barwick

#94Michael Paquier
michael@paquier.xyz
In reply to: Ian Lawrence Barwick (#93)
Re: Allow file inclusion in pg_hba and pg_ident files

On Sun, Nov 27, 2022 at 09:49:31PM +0900, Ian Lawrence Barwick wrote:

Thanks for the quick update!

FWIW, I do intend to tackle this last part ASAP, as the last piece to
commit to get the full picture in the tree.
--
Michael

#95Michael Paquier
michael@paquier.xyz
In reply to: Julien Rouhaud (#92)
1 attachment(s)
Re: Allow file inclusion in pg_hba and pg_ident files

On Sun, Nov 27, 2022 at 07:04:46PM +0800, Julien Rouhaud wrote:

And here's the rebased patch for the TAP tests. I will switch the CF entry to
Needs Review.

I have been looking at that, and applied the part 1 of the test for
the positive tests to cover the basic ground I'd like to see covered
for this feature. I have done quite a few changes to this part, like
reworking the way the incrementation of the counters is done for the
rule/map numbers and the line numbers of each file. The idea to use a
global dictionary to track the current number of lines was rather
clear, so I have kept that. add_{hba,ident}_line() rely on the
files included in a directory to be ordered in the same way as the
backend, and I am fine to live with that.

+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g;    # reduce /./ to /
+$data_dir =~ s/\/\//\//g;      # reduce // to /
+$data_dir =~ s/\/$//;          # remove trailing /
+$data_dir =~ s/\\/\//g;        # change \ to /
The mysticism around the conversion of the data directory path is
something I'd like to avoid, or I suspect that we are going to have a
hard time debugging any issues in this area.  For the positive cases
committed, I have gone through the approach of using the base name of
the configuration file to avoid those compatibility issues.  Wouldn't
it better to do the same for the expected error messages, switching to
an approach where we only check for the configuration file name in all
these regexps?
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
generate_log_err_rows() does not strike me as a good interface.  The
issues I am pointed at here is it depends directly on the result
returned by add_hba_line() or add_ident_line() when the caller passes
down an error string.  As far as I get it, this interface is done
as-is to correctly increment the line number of one file, and I'd like
to believe that add_{hba,ident}_line() should be kept designed so as
they only return the expected matching entry in their catalog views,
without any code path returning something else (in your case the
triplet [ $filename, $fileline, $err_str ]).  That makes the whole
harder to understand.

Attached is a rebased patch of the rest. With everything we have
dealt with in this CF, perhaps it would be better to mark this entry
as committed and switch to a new thread where the negative TAP tests
could be discussed? It looks like the part for the error pattern
checks compared with the logs could be the easiest portion of what's
remaining.
--
Michael

Attachments:

v24-0001-Add-regression-tests-for-file-inclusion-in-HBA-e.patchtext/x-diff; charset=us-asciiDownload
From adb076649534bf9ef3020c8a000ed7fd269b13b7 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 28 Nov 2022 16:08:15 +0900
Subject: [PATCH v24] Add regression tests for file inclusion in HBA end ident
 configuration files

This covers the error tests.

Author: Julien Rouhaud
Reviewed-by: FIXME
Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya@jrouhaud
---
 .../authentication/t/004_file_inclusion.pl    | 454 +++++++++++++++++-
 1 file changed, 452 insertions(+), 2 deletions(-)

diff --git a/src/test/authentication/t/004_file_inclusion.pl b/src/test/authentication/t/004_file_inclusion.pl
index c420f3ebca..4a49d55c6a 100644
--- a/src/test/authentication/t/004_file_inclusion.pl
+++ b/src/test/authentication/t/004_file_inclusion.pl
@@ -33,6 +33,10 @@ my %line_counters = ('hba_rule' => 0, 'ident_rule' => 0);
 # the general hba rule number as an include directive generates no data
 # in pg_hba_file_rules.
 #
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+#
 # This function returns the entry of pg_hba_file_rules expected when this
 # is loaded by the backend.
 sub add_hba_line
@@ -40,6 +44,7 @@ sub add_hba_line
 	my $node     = shift;
 	my $filename = shift;
 	my $entry    = shift;
+	my $err_str  = shift;
 	my $globline;
 	my $fileline;
 	my @tokens;
@@ -58,11 +63,27 @@ sub add_hba_line
 	$fileline = ++$line_counters{$filename};
 
 	# Include directive, that does not generate a view entry.
-	return '' if ($entry =~ qr/^include/);
+	if ($entry =~ qr/^include/)
+	{
+		if (defined $err_str)
+		{
+			return [ $filename, $fileline, $err_str ];
+		}
+		else
+		{
+			return '';
+		}
+	}
 
 	# Increment pg_hba_file_rules.rule_number and save it.
 	$globline = ++$line_counters{'hba_rule'};
 
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [ $filename, $fileline, $err_str ];
+	}
+
 	# Generate the expected pg_hba_file_rules line
 	@tokens    = split(/ /, $entry);
 	$tokens[1] = '{' . $tokens[1] . '}';    # database
@@ -91,6 +112,10 @@ sub add_hba_line
 # the general map number as an include directive generates no data in
 # pg_ident_file_mappings.
 #
+# If an err_str is provided, it returns an arrayref containing the provided
+# filename, the current line number in that file and the provided err_str.  The
+# err_str has to be a valid regex string.
+#
 # This works pretty much the same as add_hba_line() above, except that it
 # returns an entry to match with pg_ident_file_mappings.
 sub add_ident_line
@@ -98,6 +123,7 @@ sub add_ident_line
 	my $node     = shift;
 	my $filename = shift;
 	my $entry    = shift;
+	my $err_str  = shift;
 	my $globline;
 	my $fileline;
 	my @tokens;
@@ -116,11 +142,27 @@ sub add_ident_line
 	$fileline = ++$line_counters{$filename};
 
 	# Include directive, that does not generate a view entry.
-	return '' if ($entry =~ qr/^include/);
+	if ($entry =~ qr/^include/)
+	{
+		if (defined $err_str)
+		{
+			return [ $filename, $fileline, $err_str ];
+		}
+		else
+		{
+			return '';
+		}
+	}
 
 	# Increment pg_ident_file_mappings.map_number and get it.
 	$globline = ++$line_counters{'ident_rule'};
 
+	# If caller provided an err_str, just returns the needed metadata
+	if (defined $err_str)
+	{
+		return [ $filename, $fileline, $err_str ];
+	}
+
 	# Generate the expected pg_ident_file_mappings line
 	@tokens = split(/ /, $entry);
 	# Append empty error
@@ -145,6 +187,12 @@ $node->start;
 
 my $data_dir = $node->data_dir;
 
+# Normalize the data directory for Windows
+$data_dir =~ s/\/\.\//\//g;    # reduce /./ to /
+$data_dir =~ s/\/\//\//g;      # reduce // to /
+$data_dir =~ s/\/$//;          # remove trailing /
+$data_dir =~ s/\\/\//g;        # change \ to /
+
 note "Generating HBA structure with include directives";
 
 my $hba_expected   = '';
@@ -295,4 +343,406 @@ $contents = $node->safe_psql(
  FROM pg_ident_file_mappings ORDER BY map_number));
 is($contents, $ident_expected, 'check contents of pg_ident_file_mappings');
 
+# Delete pg_hba.conf and pg_ident.conf from the given node and add minimal
+# entries to allow authentication.
+sub reset_auth_files
+{
+	my $node = shift;
+
+	unlink("$data_dir/$hba_file");
+	unlink("$data_dir/$ident_file");
+
+	%line_counters = ('hba_rule' => 0, 'ident_rule' => 0);
+
+	return add_hba_line($node, "$hba_file", 'local all all trust');
+}
+
+# Generate a list of expected error regex for the given array of error
+# conditions, as generated by add_hba_line/add_ident_line with an err_str.
+#
+# 2 regex are generated per array entry: one for the given err_str, and one for
+# the expected line in the specific file.  Since all lines are independant,
+# there's no guarantee that a specific failure regex and the per-line regex
+# will match the same error.  Calling code should add at least one test with a
+# single error to make sure that the line number / file name is correct.
+#
+# On top of that, an extra line is generated for the general failure to process
+# the main auth file.
+sub generate_log_err_patterns
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $is_hba_err = shift;
+	my @errors;
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str  = @{$arr}[2];
+
+		push @errors, qr/$err_str/;
+
+		# Context messages with the file / line location aren't always emitted
+		if (    $err_str !~ /maximum nesting depth exceeded/
+			and $err_str !~ /could not open file/)
+		{
+			push @errors,
+			  qr/line $fileline of configuration file "$data_dir\/$filename"/;
+		}
+	}
+
+	push @errors, qr/could not load $data_dir\/$hba_file/ if ($is_hba_err);
+
+	return \@errors;
+}
+
+# Generate the expected output for the auth file view error reporting (file
+# name, file line, error), for the given array of error conditions, as
+# generated generated by add_hba_line/add_ident_line with an err_str.
+sub generate_log_err_rows
+{
+	my $node       = shift;
+	my $raw_errors = shift;
+	my $exp_rows   = '';
+
+	foreach my $arr (@{$raw_errors})
+	{
+		my $filename = @{$arr}[0];
+		my $fileline = @{$arr}[1];
+		my $err_str  = @{$arr}[2];
+
+		$exp_rows .= "\n" if ($exp_rows ne "");
+
+		# Unescape regex patterns if any
+		$err_str =~ s/\\([\(\)])/$1/g;
+		$exp_rows .= "|$data_dir\/$filename|$fileline|$err_str";
+	}
+
+	return $exp_rows;
+}
+
+# Reset the main auth files, append the given payload to the given config file,
+# and check that the instance cannot start, raising the expected error line(s).
+sub start_errors_like
+{
+	my $node        = shift;
+	my $file        = shift;
+	my $payload     = shift;
+	my $pattern     = shift;
+	my $should_fail = shift;
+
+	reset_auth_files($node);
+	$node->append_conf($file, $payload);
+
+	unlink($node->logfile);
+	my $ret =
+	  PostgreSQL::Test::Utils::system_log('pg_ctl', '-D', $data_dir,
+		'-l', $node->logfile, 'start');
+
+	if ($should_fail)
+	{
+		ok($ret != 0, "Cannot start postgres with faulty $file");
+	}
+	else
+	{
+		ok($ret == 0, "postgres can start with faulty $file");
+	}
+
+	my $log_contents = slurp_file($node->logfile);
+
+	foreach (@{$pattern})
+	{
+		like($log_contents, $_, "Expected failure found in the logs");
+	}
+
+	if (not $should_fail)
+	{
+		# We can't simply call $node->stop here as the call is optimized out
+		# when the server isn't started with $node->start.
+		my $ret =
+		  PostgreSQL::Test::Utils::system_log('pg_ctl', '-D',
+			$data_dir, 'stop', '-m', 'fast');
+		ok($ret == 0, "Could stop postgres");
+	}
+}
+
+
+note "test log reporting for invalid data";
+
+reset_auth_files($node);
+$node->restart('fast');
+$node->connect_ok('dbname=postgres',
+	'Connection ok after resetting auth files');
+
+$node->stop('fast');
+
+start_errors_like(
+	$node,
+	$hba_file,
+	"include ../not_a_file",
+	[
+		qr/could not open file "$data_dir\/not_a_file": No such file or directory/,
+		qr/could not load $data_dir\/$hba_file/
+	],
+	1);
+
+# include_dir, single included file
+mkdir("$data_dir/hba_inc_fail");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "local all all reject");
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "not_a_token");
+start_errors_like(
+	$node,
+	$hba_file,
+	"include_dir ../hba_inc_fail",
+	[
+		qr/invalid connection type "not_a_token"/,
+		qr/line 4 of configuration file "$data_dir\/hba_inc_fail\/inc_dir\.conf"/,
+		qr/could not load $data_dir\/$hba_file/
+	],
+	1);
+
+# include_dir, single included file with nested inclusion
+unlink("$data_dir/hba_inc_fail/inc_dir.conf");
+my @hba_raw_errors_step1;
+
+add_hba_line($node, "hba_inc_fail/inc_dir.conf", "include file1");
+
+add_hba_line($node, "hba_inc_fail/file1", "include file2");
+add_hba_line($node, "hba_inc_fail/file2", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file2", "include file3");
+
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+add_hba_line($node, "hba_inc_fail/file3", "local all all reject");
+push @hba_raw_errors_step1,
+  add_hba_line(
+	$node, "hba_inc_fail/file3",
+	"local all all zuul",
+	'invalid authentication method "zuul"');
+
+start_errors_like(
+	$node, $hba_file,
+	"include_dir ../hba_inc_fail",
+	generate_log_err_patterns($node, \@hba_raw_errors_step1, 1), 1);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @hba_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local",
+	"end-of-line before database specification");
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local,host",
+	"multiple values specified for connection type");
+push @hba_raw_errors_step2,
+  add_hba_line($node, "hba_if_exists.conf", "local all",
+	"end-of-line before role specification");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node, "hba_if_exists.conf",
+	"local all all",
+	"end-of-line before authentication method");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node, "hba_if_exists.conf",
+	"host all all test/42",
+	'specifying both host name and CIDR mask is invalid: "test/42"');
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node,
+	"hba_if_exists.conf",
+	'local @dbnames_fails.conf all reject',
+	"could not open file \"$data_dir/dbnames_fails.conf\": No such file or directory"
+  );
+
+add_hba_line($node, "hba_if_exists.conf", "include recurse.conf");
+push @hba_raw_errors_step2,
+  add_hba_line(
+	$node,
+	"recurse.conf",
+	"include recurse.conf",
+	"could not open file \"$data_dir/recurse.conf\": maximum nesting depth exceeded"
+  );
+
+# Generate the regex for the expected errors in the logs.  There's no guarantee
+# that the generated "line X of file..." will be emitted for the expected line,
+# but previous tests already ensured that the correct line number / file name
+# was emitted, so ensuring that there's an error in all expected lines is
+# enough here.
+my $expected_errors =
+  generate_log_err_patterns($node, \@hba_raw_errors_step2, 1);
+
+# Not an error, but it should raise a message in the logs.  Manually add an
+# extra log message to detect
+add_hba_line($node, "hba_if_exists.conf", "include_if_exists if_exists_none");
+push @{$expected_errors},
+  qr/skipping missing authentication file "$data_dir\/if_exists_none"/;
+
+start_errors_like($node, $hba_file, "include_if_exists ../hba_if_exists.conf",
+	$expected_errors, 1);
+
+# Mostly the same, but for ident files
+reset_auth_files($node);
+
+my @ident_raw_errors_step1;
+
+# include_dir, single included file with nested inclusion
+mkdir("$data_dir/ident_inc_fail");
+add_ident_line($node, "ident_inc_fail/inc_dir.conf", "include file1");
+
+add_ident_line($node, "ident_inc_fail/file1", "include file2");
+add_ident_line($node, "ident_inc_fail/file2", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file2", "include file3");
+
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+add_ident_line($node, "ident_inc_fail/file3", "ok ok ok");
+push @ident_raw_errors_step1,
+  add_ident_line(
+	$node, "ident_inc_fail/file3",
+	"failmap /(fail postgres",
+	'invalid regular expression "\(fail": parentheses \(\) not balanced');
+
+start_errors_like(
+	$node, $ident_file,
+	"include_dir ../ident_inc_fail",
+	generate_log_err_patterns($node, \@ident_raw_errors_step1, 0), 0);
+
+# start_errors_like will reset the main auth files, so the previous error won't
+# occur again.  We keep it around as we will put back both bogus inclusions for
+# the tests at step 3.
+my @ident_raw_errors_step2;
+
+# include_if_exists, with various problems
+push @ident_raw_errors_step2,
+  add_ident_line($node, "ident_if_exists.conf", "map",
+	"missing entry at end of line");
+push @ident_raw_errors_step2,
+  add_ident_line($node, "ident_if_exists.conf", "map1,map2",
+	"multiple values in ident field");
+push @ident_raw_errors_step2,
+  add_ident_line(
+	$node,
+	"ident_if_exists.conf",
+	'map @osnames_fails.conf postgres',
+	"could not open file \"$data_dir/osnames_fails.conf\": No such file or directory"
+  );
+
+add_ident_line($node, "ident_if_exists.conf", "include ident_recurse.conf");
+push @ident_raw_errors_step2,
+  add_ident_line(
+	$node,
+	"ident_recurse.conf",
+	"include ident_recurse.conf",
+	"could not open file \"$data_dir/ident_recurse.conf\": maximum nesting depth exceeded"
+  );
+
+start_errors_like(
+	$node, $ident_file, "include_if_exists ../ident_if_exists.conf",
+	# There's no guarantee that the generated "line X of file..." will be
+	# emitted for the expected line, but previous tests already ensured that
+	# the correct line number / file name was emitted, so ensuring that there's
+	# an error in all expected lines is enough here.
+	generate_log_err_patterns($node, \@ident_raw_errors_step2, 0),
+	0);
+
+
+note "test reporting of various error scenario";
+
+reset_auth_files($node);
+
+$node->start;
+$node->connect_ok('dbname=postgres', 'Can connect after an auth file reset');
+
+is( $node->safe_psql(
+		'postgres',
+		'SELECT count(*) FROM pg_hba_file_rules WHERE error IS NOT NULL'),
+	qq(0),
+	'No error expected in pg_hba_file_rules');
+
+add_ident_line($node, $ident_file, '');
+is( $node->safe_psql(
+		'postgres',
+		'SELECT count(*) FROM pg_ident_file_mappings WHERE error IS NOT NULL'
+	),
+	qq(0),
+	'No error expected in pg_ident_file_mappings');
+
+# The instance could be restarted and no error is detected.  Now check if the
+# build is compatible with the view error reporting (EXEC_BACKEND / win32 will
+# fail when trying to connect as they always rely on the current auth files
+# content)
+my @hba_raw_errors;
+
+push @hba_raw_errors,
+  add_hba_line($node, $hba_file, "include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory"
+  );
+
+my ($stdout, $stderr);
+my $cmdret = $node->psql(
+	'postgres', 'SELECT 1',
+	stdout => \$stdout,
+	stderr => \$stderr);
+
+if ($cmdret != 0)
+{
+	# Connection failed.  Bail out, but make sure to raise a failure if it
+	# didn't fail for the expected hba file modification.
+	like(
+		$stderr,
+		qr/connection to server.* failed: FATAL:  could not load $data_dir\/$hba_file/,
+		"Connection failed due to loading an invalid hba file");
+
+	done_testing();
+	diag(
+		"Build not compatible with auth file view error reporting, bail out.\n"
+	);
+	exit;
+}
+
+# Combine errors generated at step 2, in the same order.
+$node->append_conf($hba_file, "include_dir ../hba_inc_fail");
+push @hba_raw_errors, @hba_raw_errors_step1;
+
+$node->append_conf($hba_file, "include_if_exists ../hba_if_exists.conf");
+push @hba_raw_errors, @hba_raw_errors_step2;
+
+$hba_expected = generate_log_err_rows($node, \@hba_raw_errors);
+is( $node->safe_psql(
+		'postgres',
+		'SELECT rule_number, file_name, line_number, error FROM pg_hba_file_rules'
+		  . ' WHERE error IS NOT NULL ORDER BY rule_number'),
+	qq($hba_expected),
+	'Detected all error in hba file');
+
+# and do the same for pg_ident
+my @ident_raw_errors;
+
+push @ident_raw_errors,
+  add_ident_line(
+	$node,
+	$ident_file,
+	"include ../not_a_file",
+	"could not open file \"$data_dir/not_a_file\": No such file or directory"
+  );
+
+$node->append_conf($ident_file, "include_dir ../ident_inc_fail");
+push @ident_raw_errors, @ident_raw_errors_step1;
+
+$node->append_conf($ident_file, "include_if_exists ../ident_if_exists.conf");
+push @ident_raw_errors, @ident_raw_errors_step2;
+
+$ident_expected = generate_log_err_rows($node, \@ident_raw_errors);
+is( $node->safe_psql(
+		'postgres',
+		'SELECT map_number, file_name, line_number, error FROM pg_ident_file_mappings'
+		  . ' WHERE error IS NOT NULL ORDER BY map_number'),
+	qq($ident_expected),
+	'Detected all error in ident file');
+
 done_testing();
-- 
2.38.1

#96Michael Paquier
michael@paquier.xyz
In reply to: Michael Paquier (#95)
Re: Allow file inclusion in pg_hba and pg_ident files

On Mon, Nov 28, 2022 at 04:12:40PM +0900, Michael Paquier wrote:

Attached is a rebased patch of the rest. With everything we have
dealt with in this CF, perhaps it would be better to mark this entry
as committed and switch to a new thread where the negative TAP tests
could be discussed?

For now, I have marked the CF entry as committed.
--
Michael