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.
>From ab685e1db37239df06ccdd7be8cfc14789e73e8c Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouh...@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

>From 9fff95d2c86f2e777401561623e922ec4ecbc245 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouh...@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

>From 6b9642a2f5d202ce04c73d2ac69df7be03624ee9 Mon Sep 17 00:00:00 2001
From: Julien Rouhaud <julien.rouh...@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

Reply via email to