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.
>From 681c16db9fcb4e27ebffffba28e6a0f2134de071 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 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

>From 84759803f64d3532341d0bf877e2a3a1e31554f2 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 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

>From 791e7c62e81523db751c92fde2b9399b2c0d3112 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 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

Reply via email to