public inbox for [email protected]  
help / color / mirror / Atom feed
From: Julien Rouhaud <[email protected]>
Subject: [PATCH v7 1/2] Allow file inclusion in pg_hba and pg_ident files.
Date: Mon, 21 Feb 2022 15:45:26 +0800

Catversion is bumped.

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

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


--o45jk7yo2ulu7ifa
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment;
	filename="v7-0002-POC-Add-a-pg_hba_matches-function.patch"



view thread (17+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected]
  Subject: Re: [PATCH v7 1/2] Allow file inclusion in pg_hba and pg_ident files.
  In-Reply-To: <no-message-id-1861448@localhost>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox