diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index 32d5d45863..3b2a0f8fb0 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -89,8 +89,8 @@ - Each record specifies a connection type, a client IP address range - (if relevant for the connection type), a database name, a user name, + Each authentication record specifies a connection type, a client IP address + range (if relevant for the connection type), a database name, a user name, and the authentication method to be used for connections matching these parameters. The first record with a matching connection type, client address, requested database, and user name is used to perform @@ -100,20 +100,38 @@ access is denied. + + Each record can be an include directive or an authentication record. + Include directives specify files that can be included, that contain + additional records. The records will be inserted in place of the + include records. These records only contain two fields: + include, include_if_exists or + include_dir directive and the file or directory to be + included. The file or directory can be a relative of absolute path, and can + be double-quoted. For the include_dir form, all files + not starting with a . and ending with + .conf will be included. Multiple files within an include + directory are processed in file name order (according to C locale rules, + i.e., numbers before letters, and uppercase letters before lowercase ones). + + A record can have several formats: -local database user auth-method auth-options -host database user address auth-method auth-options -hostssl database user address auth-method auth-options -hostnossl database user address auth-method auth-options -hostgssenc database user address auth-method auth-options -hostnogssenc database user address auth-method auth-options -host database user IP-address IP-mask auth-method auth-options -hostssl database user IP-address IP-mask auth-method auth-options -hostnossl database user IP-address IP-mask auth-method auth-options -hostgssenc database user IP-address IP-mask auth-method auth-options -hostnogssenc database user IP-address IP-mask auth-method auth-options +local database user auth-method auth-options +host database user address auth-method auth-options +hostssl database user address auth-method auth-options +hostnossl database user address auth-method auth-options +hostgssenc database user address auth-method auth-options +hostnogssenc database user address auth-method auth-options +host database user IP-address IP-mask auth-method auth-options +hostssl database user IP-address IP-mask auth-method auth-options +hostnossl database user IP-address IP-mask auth-method auth-options +hostgssenc database user IP-address IP-mask auth-method auth-options +hostnogssenc database user IP-address IP-mask auth-method auth-options +include file +include_if_exists file +include_dir directory The meaning of the fields is as follows: @@ -655,6 +673,39 @@ openssl x509 -in myclient.crt -noout --subject -nameopt RFC2253 | sed "s/^subjec + + + include + + + This line will be replaced by the contents of the given file. + + + + + + include_if_exists + + + This line will be replaced by the content of the given file if the + file exists. Otherwise, a message is logged to indicate that the file + has been skipped. + + + + + + include_dir + + + This line will be replaced by the contents of all the files found in + the directory, if they don't start with a . and end + with .conf, processed in file name order (according + to C locale rules, i.e., numbers before letters, and uppercase letters + before lowercase ones). + + + @@ -863,9 +914,12 @@ local db1,db2,@demodbs all md5 cluster's data directory. (It is possible to place the map file elsewhere, however; see the configuration parameter.) - The ident map file contains lines of the general form: + The ident map file contains lines of the general forms: map-name system-username database-username +include file +include_if_exists file +include_dir directory Comments, whitespace and line continuations are handled in the same way as in pg_hba.conf. The @@ -875,6 +929,10 @@ local db1,db2,@demodbs all md5 database user name. The same map-name can be used repeatedly to specify multiple user-mappings within a single map. + + As for pg_hba.conf, the lines in this file can + be include directives, following the same rules. + There is no restriction regarding how many database users a given operating system user can correspond to, nor vice versa. Thus, entries diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index 7c716fe327..d38b42c5cd 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -1002,12 +1002,21 @@ + + + file_name text + + + Name of the file containing this rule + + + line_number int4 - Line number of this rule in pg_hba.conf + Line number of this rule in file_name @@ -1152,12 +1161,21 @@ + + + file_name text + + + Name of the file containing this map + + + line_number int4 - Line number of this map in pg_ident.conf + Line number of this map in file_name diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index 1bd151c941..862ec18e91 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -463,16 +463,74 @@ next_field_expand(const char *filename, char **lineptr, return tokens; } +/* + * tokenize_include_file + * Include a file from another file into an hba "field". + * + * Opens and tokenises a file included from another authentication file + * with one of the include records ("include", "include_if_exists" or + * "include_dir"), and assign all values found to an existing list of + * list of AuthTokens. + * + * All new tokens are allocated in the memory context dedicated to the + * tokenization, aka tokenize_context. + * + * If missing_ok is true, ignore a missing file. + * + * In event of an error, log a message at ereport level elevel, and also + * set *err_msg to a string describing the error. Note that the result + * may be non-NIL anyway, so *err_msg must be tested to determine whether + * there was an error. + */ +static void +tokenize_include_file(const char *outer_filename, + const char *inc_filename, + List **tok_lines, + int elevel, + int depth, + bool missing_ok, + char **err_msg) +{ + char *inc_fullname; + FILE *inc_file; + + inc_fullname = AbsoluteConfigLocation(inc_filename, outer_filename); + inc_file = open_auth_file(inc_fullname, elevel, depth, err_msg); + + if (!inc_file) + { + if (errno == ENOENT && missing_ok) + { + ereport(elevel, + (errmsg("skipping missing authentication file \"%s\"", + inc_fullname))); + *err_msg = NULL; + pfree(inc_fullname); + return; + } + + /* error in err_msg, so leave and report */ + pfree(inc_fullname); + Assert(err_msg); + return; + } + + tokenize_auth_file(inc_fullname, inc_file, tok_lines, elevel, + depth); + free_auth_file(inc_file, depth); + pfree(inc_fullname); +} + /* * tokenize_expand_file * Expand a file included from another file into an hba "field" * * Opens and tokenises a file included from another HBA config file with @, * and returns all values found therein as a flat list of AuthTokens. If a - * @-token is found, recursively expand it. The newly read tokens are - * appended to "tokens" (so that foo,bar,@baz does what you expect). - * All new tokens are allocated in the memory context dedicated to the - * list of TokenizedAuthLines, aka tokenize_context. + * @-token or include record is found, recursively expand it. The newly + * read tokens are appended to "tokens" (so that foo,bar,@baz does what you + * expect). All new tokens are allocated in the memory context dedicated + * to the list of TokenizedAuthLines, aka tokenize_context. * * In event of an error, log a message at ereport level elevel, and also * set *err_msg to a string describing the error. Note that the result @@ -502,7 +560,10 @@ tokenize_expand_file(List *tokens, return tokens; } - /* There is possible recursion here if the file contains @ */ + /* + * There is possible recursion here if the file contains @ or an include + * records. + */ tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel, depth); @@ -706,6 +767,8 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, while (!feof(file) && !ferror(file)) { + TokenizedAuthLine *tok_line; + MemoryContext oldcxt; char *lineptr; List *current_line = NIL; char *err_msg = NULL; @@ -763,8 +826,6 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, /* add field to line, unless we are at EOL or comment start */ if (current_field != NIL) { - MemoryContext oldcxt; - /* * lappend() may do its own allocations, so move to the * context for the list of tokens. @@ -776,24 +837,115 @@ tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, } /* - * Reached EOL; emit line to TokenizedAuthLine list unless it's boring + * Reached EOL; no need to emit line to TokenizedAuthLine list if it's + * boring. */ - if (current_line != NIL || err_msg != NULL) - { - TokenizedAuthLine *tok_line; - MemoryContext oldcxt; + if (current_line == NIL && err_msg == NULL) + goto next_line; - oldcxt = MemoryContextSwitchTo(tokenize_context); - tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine)); - tok_line->fields = current_line; - tok_line->file_name = pstrdup(filename); - tok_line->line_num = line_number; - tok_line->raw_line = pstrdup(buf.data); - tok_line->err_msg = err_msg ? pstrdup(err_msg) : NULL; - *tok_lines = lappend(*tok_lines, tok_line); - MemoryContextSwitchTo(oldcxt); + /* If the line is valid, check if that's an include directive */ + if (err_msg == NULL && list_length(current_line) == 2) + { + AuthToken *first, + *second; + + first = linitial(linitial_node(List, current_line)); + second = linitial(lsecond_node(List, current_line)); + + if (strcmp(first->string, "include") == 0) + { + tokenize_include_file(filename, second->string, tok_lines, + elevel, depth + 1, false, &err_msg); + + if (err_msg) + goto process_line; + + /* + * tokenize_auth_file() has taken care of creating the + * TokenizedAuthLines. + */ + goto next_line; + } + else if (strcmp(first->string, "include_dir") == 0) + { + char **filenames; + char *dir_name = second->string; + int num_filenames; + StringInfoData err_buf; + + filenames = GetConfFilesInDir(dir_name, filename, elevel, + &num_filenames, &err_msg); + + if (!filenames) + { + /* the error is in err_msg, so create an entry */ + goto process_line; + } + + initStringInfo(&err_buf); + for (int i = 0; i < num_filenames; i++) + { + tokenize_include_file(filename, filenames[i], tok_lines, + elevel, depth + 1, false, &err_msg); + /* cumulate errors if any */ + if (err_msg) + { + if (err_buf.len > 0) + appendStringInfoChar(&err_buf, '\n'); + appendStringInfoString(&err_buf, err_msg); + } + } + + /* clean up things */ + for (int i = 0; i < num_filenames; i++) + pfree(filenames[i]); + pfree(filenames); + + /* + * If there were no errors, the line is fully processed, + * bypass the general TokenizedAuthLine processing. + */ + if (err_buf.len == 0) + goto next_line; + + /* Otherwise, process the cumulated errors, if any. */ + err_msg = err_buf.data; + goto process_line; + } + else if (strcmp(first->string, "include_if_exists") == 0) + { + + tokenize_include_file(filename, second->string, tok_lines, + elevel, depth + 1, true, &err_msg); + if (err_msg) + goto process_line; + + /* + * tokenize_auth_file() has taken care of creating the + * TokenizedAuthLines. + */ + goto next_line; + } } +process_line: + + /* + * General processing: report the error if any and emit line to the + * TokenizedAuthLine. This is saved in the memory context dedicated + * to this list. + */ + oldcxt = MemoryContextSwitchTo(tokenize_context); + tok_line = (TokenizedAuthLine *) palloc0(sizeof(TokenizedAuthLine)); + tok_line->fields = current_line; + tok_line->file_name = pstrdup(filename); + tok_line->line_num = line_number; + tok_line->raw_line = pstrdup(buf.data); + tok_line->err_msg = err_msg ? pstrdup(err_msg) : NULL; + *tok_lines = lappend(*tok_lines, tok_line); + MemoryContextSwitchTo(oldcxt); + +next_line: line_number += continuations + 1; callback_arg.linenum = line_number; } diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample index 5f3f63eb0c..095e3b4cc0 100644 --- a/src/backend/libpq/pg_hba.conf.sample +++ b/src/backend/libpq/pg_hba.conf.sample @@ -5,6 +5,10 @@ # documentation for a complete description of this file. A short # synopsis follows. # +# ---------------------- +# Authentication Records +# ---------------------- +# # This file controls: which hosts are allowed to connect, how clients # are authenticated, which PostgreSQL user names they can use, which # databases they can access. Records take one of these forms: @@ -64,11 +68,34 @@ # its special character, and just match a database or username with # that name. # +# --------------- +# Include Records +# --------------- +# +# This file allows the inclusion of external files or directories holding +# more records, using the following keywords: +# +# include FILE +# include_if_exists FILE +# include_dir DIRECTORY +# +# FILE is the file name to include, and DIR is the directory name containing +# the file(s) to include. Any file in a directory will be loaded if suffixed +# with ".conf". The files of a directory are ordered by name. +# include_if_exists ignores missing files. FILE and DIRECTORY can be +# specified as a relative or an absolute path, and can be double-quoted if +# they contain spaces. +# +# ------------- +# Miscellaneous +# ------------- +# # This file is read on server startup and when the server receives a # SIGHUP signal. If you edit the file on a running system, you have to # SIGHUP the server for the changes to take effect, run "pg_ctl reload", # or execute "SELECT pg_reload_conf()". # +# ---------------------------------- # Put your actual configuration here # ---------------------------------- # diff --git a/src/backend/libpq/pg_ident.conf.sample b/src/backend/libpq/pg_ident.conf.sample index a5870e6448..5d32684b28 100644 --- a/src/backend/libpq/pg_ident.conf.sample +++ b/src/backend/libpq/pg_ident.conf.sample @@ -1,6 +1,10 @@ # PostgreSQL User Name Maps # ========================= # +# --------------- +# Mapping Records +# --------------- +# # Refer to the PostgreSQL documentation, chapter "Client # Authentication" for a complete description. A short synopsis # follows. @@ -31,6 +35,28 @@ # system user names and PostgreSQL user names are the same, you don't # need anything in this file. # +# --------------- +# Include Records +# --------------- +# +# This file allows the inclusion of external files or directories holding +# more records, using the following keywords: +# +# include FILE +# include_if_exists FILE +# include_dir DIRECTORY +# +# FILE is the file name to include, and DIR is the directory name containing +# the file(s) to include. Any file in a directory will be loaded if suffixed +# with ".conf". The files of a directory are ordered by name. +# include_if_exists ignores missing files. FILE and DIRECTORY can be +# specified as a relative or an absolute path, and can be double-quoted if +# they contain spaces. +# +# ------------------------------- +# Miscellaneous +# ------------------------------- +# # This file is read on server startup and when the postmaster receives # a SIGHUP signal. If you edit the file on a running system, you have # to SIGHUP the postmaster for the changes to take effect. You can diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c index 87996da487..633eda30d3 100644 --- a/src/backend/utils/adt/hbafuncs.c +++ b/src/backend/utils/adt/hbafuncs.c @@ -26,12 +26,12 @@ static ArrayType *get_hba_options(HbaLine *hba); static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, - int rule_number, int lineno, HbaLine *hba, - const char *err_msg); + int rule_number, char *filename, int lineno, + HbaLine *hba, const char *err_msg); static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc); static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, - int map_number, int lineno, IdentLine *ident, - const char *err_msg); + int map_number, char *filename, int lineno, + IdentLine *ident, const char *err_msg); static void fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc); @@ -159,7 +159,7 @@ get_hba_options(HbaLine *hba) } /* Number of columns in pg_hba_file_rules view */ -#define NUM_PG_HBA_FILE_RULES_ATTS 10 +#define NUM_PG_HBA_FILE_RULES_ATTS 11 /* * fill_hba_line @@ -168,7 +168,8 @@ get_hba_options(HbaLine *hba) * tuple_store: where to store data * tupdesc: tuple descriptor for the view * rule_number: unique identifier among all valid rules - * lineno: pg_hba.conf line number (must always be valid) + * filename: configuration file name (must always be valid) + * lineno: line number of configuration file (must always be valid) * hba: parsed line data (can be NULL, in which case err_msg should be set) * err_msg: error message (NULL if none) * @@ -177,7 +178,7 @@ get_hba_options(HbaLine *hba) */ static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, - int rule_number, int lineno, HbaLine *hba, + int rule_number, char *filename, int lineno, HbaLine *hba, const char *err_msg) { Datum values[NUM_PG_HBA_FILE_RULES_ATTS]; @@ -203,6 +204,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, else values[index++] = Int32GetDatum(rule_number); + /* file_name */ + values[index++] = CStringGetTextDatum(filename); + /* line_number */ values[index++] = Int32GetDatum(lineno); @@ -346,7 +350,7 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, else { /* no parsing result, so set relevant fields to nulls */ - memset(&nulls[2], true, (NUM_PG_HBA_FILE_RULES_ATTS - 3) * sizeof(bool)); + memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 4) * sizeof(bool)); } /* error */ @@ -402,7 +406,8 @@ fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc) rule_number++; fill_hba_line(tuple_store, tupdesc, rule_number, - tok_line->line_num, hbaline, tok_line->err_msg); + tok_line->file_name, tok_line->line_num, hbaline, + tok_line->err_msg); } /* Free tokenizer memory */ @@ -439,7 +444,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS) } /* Number of columns in pg_ident_file_mappings view */ -#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS 6 +#define NUM_PG_IDENT_FILE_MAPPINGS_ATTS 7 /* * fill_ident_line: build one row of pg_ident_file_mappings view, add it to @@ -448,7 +453,8 @@ pg_hba_file_rules(PG_FUNCTION_ARGS) * tuple_store: where to store data * tupdesc: tuple descriptor for the view * map_number: unique identifier among all valid maps - * lineno: pg_ident.conf line number (must always be valid) + * filename: configuration file name (must always be valid) + * lineno: line number of configuration file (must always be valid) * ident: parsed line data (can be NULL, in which case err_msg should be set) * err_msg: error message (NULL if none) * @@ -457,7 +463,7 @@ pg_hba_file_rules(PG_FUNCTION_ARGS) */ static void fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, - int map_number, int lineno, IdentLine *ident, + int map_number, char *filename, int lineno, IdentLine *ident, const char *err_msg) { Datum values[NUM_PG_IDENT_FILE_MAPPINGS_ATTS]; @@ -477,6 +483,9 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, else values[index++] = Int32GetDatum(map_number); + /* file_name */ + values[index++] = CStringGetTextDatum(filename); + /* line_number */ values[index++] = Int32GetDatum(lineno); @@ -489,7 +498,7 @@ fill_ident_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, else { /* no parsing result, so set relevant fields to nulls */ - memset(&nulls[2], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 3) * sizeof(bool)); + memset(&nulls[3], true, (NUM_PG_IDENT_FILE_MAPPINGS_ATTS - 4) * sizeof(bool)); } /* error */ @@ -544,8 +553,8 @@ fill_ident_view(Tuplestorestate *tuple_store, TupleDesc tupdesc) map_number++; fill_ident_line(tuple_store, tupdesc, map_number, - tok_line->line_num, identline, - tok_line->err_msg); + tok_line->file_name, tok_line->line_num, + identline, tok_line->err_msg); } /* Free tokenizer memory */ diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h index 94da0ee1d7..83f8fe46eb 100644 --- a/src/include/catalog/catversion.h +++ b/src/include/catalog/catversion.h @@ -57,6 +57,6 @@ */ /* yyyymmddN */ -#define CATALOG_VERSION_NO 202211221 +#define CATALOG_VERSION_NO 202211241 #endif diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index f15aa2dbb1..f9301b2627 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6161,16 +6161,16 @@ { oid => '3401', descr => 'show pg_hba.conf rules', proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', - proallargtypes => '{int4,int4,text,_text,_text,text,text,text,_text,text}', - proargmodes => '{o,o,o,o,o,o,o,o,o,o}', - proargnames => '{rule_number,line_number,type,database,user_name,address,netmask,auth_method,options,error}', + proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text}', + proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{rule_number,file_name,line_number,type,database,user_name,address,netmask,auth_method,options,error}', prosrc => 'pg_hba_file_rules' }, { oid => '6250', descr => 'show pg_ident.conf mappings', proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', - proallargtypes => '{int4,int4,text,text,text,text}', - proargmodes => '{o,o,o,o,o,o}', - proargnames => '{map_number,line_number,map_name,sys_name,pg_username,error}', + proallargtypes => '{int4,text,int4,text,text,text,text}', + proargmodes => '{o,o,o,o,o,o,o}', + proargnames => '{map_number,file_name,line_number,map_name,sys_name,pg_username,error}', prosrc => 'pg_ident_file_mappings' }, { oid => '1371', descr => 'view system lock information', proname => 'pg_lock_status', prorows => '1000', proretset => 't', diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 7c7adbc004..37c1c86473 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1339,6 +1339,7 @@ pg_group| SELECT pg_authid.rolname AS groname, FROM pg_authid WHERE (NOT pg_authid.rolcanlogin); pg_hba_file_rules| SELECT a.rule_number, + a.file_name, a.line_number, a.type, a.database, @@ -1348,14 +1349,15 @@ pg_hba_file_rules| SELECT a.rule_number, a.auth_method, a.options, a.error - FROM pg_hba_file_rules() a(rule_number, line_number, type, database, user_name, address, netmask, auth_method, options, error); + FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, error); pg_ident_file_mappings| SELECT a.map_number, + a.file_name, a.line_number, a.map_name, a.sys_name, a.pg_username, a.error - FROM pg_ident_file_mappings() a(map_number, line_number, map_name, sys_name, pg_username, error); + FROM pg_ident_file_mappings() a(map_number, file_name, line_number, map_name, sys_name, pg_username, error); pg_indexes| SELECT n.nspname AS schemaname, c.relname AS tablename, i.relname AS indexname,