From c7cae8f8faa9f2d90149b7494941bdba49912e99 Mon Sep 17 00:00:00 2001 From: Zsolt Parragi Date: Fri, 9 Oct 2026 14:03:18 +0000 Subject: [POC PATCH v2 4/6] Add per-rule and per-host settings to the JSON authentication files Some settings only make sense for a particular group of connections, such as the authentication timeout for connections using a slow authentication method, but a parameter can today only be global or per session. Add two GUC contexts between PGC_SIGHUP and PGC_SU_BACKEND, PGC_HOSTS and PGC_HBA, with matching sources PGC_S_HOSTS and PGC_S_HBA. A parameter in one of these contexts can additionally be set for the connections matched by a hosts entry or an hba rule, through a "settings" object on the entry or node of the JSON files. Hosts settings are applied when the entry is selected during the handshake, hba settings once the rule has matched, before authentication starts; an hba setting overrides a hosts setting for the same parameter. The values are validated when the configuration is loaded, so applying them cannot fail. authentication_timeout moves to PGC_HBA, and the authentication timer is re-armed after the rule has been matched so that a per-rule value takes effect. pg_hba_file_rules and pg_hosts_file_entries gain a settings column. --- doc/src/sgml/client-auth.sgml | 58 +++++- doc/src/sgml/config.sgml | 5 +- doc/src/sgml/runtime.sgml | 17 +- doc/src/sgml/system-views.sgml | 56 +++++- src/backend/libpq/auth.c | 14 ++ src/backend/libpq/be-secure-common.c | 20 ++ src/backend/libpq/be-secure-openssl.c | 3 + src/backend/libpq/hba.c | 34 +++- src/backend/libpq/hba_json.c | 213 +++++++++++++++++----- src/backend/nls.mk | 4 +- src/backend/utils/adt/hbafuncs.c | 49 ++++- src/backend/utils/misc/config_json.c | 97 ++++++++++ src/backend/utils/misc/guc.c | 102 +++++++++++ src/backend/utils/misc/guc_parameters.dat | 2 +- src/backend/utils/misc/guc_tables.c | 4 + src/include/catalog/pg_proc.dat | 12 +- src/include/libpq/hba.h | 6 + src/include/libpq/libpq.h | 1 + src/include/utils/config_json.h | 2 + src/include/utils/guc.h | 36 +++- src/test/authentication/t/008_json_hba.pl | 175 +++++++++++++++--- src/test/regress/expected/guc.out | 9 + src/test/regress/expected/rules.out | 6 +- src/test/regress/sql/guc.sql | 3 + src/test/ssl/t/005_json_hosts.pl | 76 ++++++-- 25 files changed, 878 insertions(+), 126 deletions(-) diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index 09d88ac948b..2e35a1bdee9 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -917,10 +917,10 @@ local db1,db2,@demodbs all scram-sha-256 The value of hba is an object with a single key, children, whose value is an array of nodes. Rather than a flat list of records, the - nodes form a tree: a node carries match conditions and authentication - options that the nodes below it inherit and can override, and a node is - tried before its parent. A node is an object with these keys, all - optional: + nodes form a tree: a node carries match conditions, authentication + options and settings that the nodes below it inherit and can override, + and a node is tried before its parent. A node is an object with these + keys, all optional: @@ -988,6 +988,14 @@ local db1,db2,@demodbs all scram-sha-256 passed as 1 or 0. + + settings + + An object mapping configuration parameter names to values. The + values are applied to every connection the node's rule matches, as + described below. + + children @@ -1027,6 +1035,12 @@ local db1,db2,@demodbs all scram-sha-256 child, or omit the key to keep the parent's. + + + settings are merged key by key; a key set in the + child replaces the inherited value for that key only. + + auth is merged key by key as well, unless the @@ -1171,8 +1185,11 @@ local db1,db2,@demodbs all scram-sha-256 An import node splices an existing file in pg_hba.conf format into the list at its position, which allows a gradual move to the tree format. The imported records - are complete rules and do not inherit anything from the nodes above - the import node. + are complete rules and do not inherit match keys or + auth from the nodes above the import node, but they + do inherit its effective settings, so a group of + legacy rules can be given settings by wrapping the import node in a + node that carries them. @@ -1190,6 +1207,32 @@ local db1,db2,@demodbs all scram-sha-256 rules before those of the file including it. + + When a record produced by a node matches a connection, the node's + effective settings are applied to the session before + authentication begins, in the hba context (see + ). Settings made this way take + precedence over the server-wide configuration and are in turn + overridden by per-database and per-role settings. Parameters that can + only be set at server start, or only at server start and reload, are + not accepted; is, and a + value set on a node applies to the authentication of the connections + that node matches. A value is written as in the top level of the + document, so an array is a list. Settings are validated when the + configuration is loaded, and a node with an unknown parameter or an + invalid value is reported as an error like any other faulty rule. As + with ALTER ROLE ... SET, a value that names a + database object, such as a tablespace, is accepted whether or not the + object exists: the settings are applied before a database is selected, + so the name cannot be checked then either, and a parameter whose + object is missing behaves as it does for any other such setting. + Since they are applied before the parameters the client sends in its + connection request, such as those set with + PGOPTIONS, a client can override them where the + parameter allows that; they are defaults for the connection, not + restrictions. + + Errors in a node, including those a pg_hba.conf record would also have, are reported with the file, the line of the @@ -1197,7 +1240,8 @@ local db1,db2,@demodbs all scram-sha-256 $.hba.children[1].children[0]. The pg_hba_file_rules view shows the derived records in evaluation order, with that path in - its node_path column. + its node_path column and the effective + settings in settings. diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml index 9a607fdeb60..9d3d00a7d40 100644 --- a/doc/src/sgml/config.sgml +++ b/doc/src/sgml/config.sgml @@ -1125,8 +1125,9 @@ include_dir 'conf.d' hung clients from occupying a connection indefinitely. If this value is specified without units, it is taken as seconds. The default is one minute (1m). - This parameter can only be set in the postgresql.conf - file or on the server command line. + This parameter can be set in the postgresql.conf + file, on the server command line, or per rule in the client + authentication configuration (see ). diff --git a/doc/src/sgml/runtime.sgml b/doc/src/sgml/runtime.sgml index 853ed446a96..a0936163062 100644 --- a/doc/src/sgml/runtime.sgml +++ b/doc/src/sgml/runtime.sgml @@ -2718,7 +2718,8 @@ openssl x509 -req -in server.csr -text -days 365 \ "globex.db.example.com": {"ssl_cert_file": "globex.crt", "ssl_key_file": "globex.key", "ssl_passphrase_command": "pass show pg/globex-key", "ssl_passphrase_command_supports_reload": true}, - "*": {"ssl_cert_file": "server.crt", "ssl_key_file": "server.key"} + "*": {"ssl_cert_file": "server.crt", "ssl_key_file": "server.key", + "settings": {"log_connections": "all"}} } } @@ -2759,6 +2760,20 @@ openssl x509 -req -in server.csr -text -days 365 \ do when is missing. + + An entry can also carry settings, an object of + configuration parameters and their values. They are applied to the + session, in the hosts context (see + ), when the entry is selected for a + connection during the handshake, whether by its host name or as the + default or /no_sni/ entry, before client + authentication. Settings of the authentication rule that then matches + the connection (see ) are applied + later and override them where both set the same parameter. Settings + are validated when the configuration is loaded, and an entry with an + unknown parameter or an invalid value is an error like any other. + + The pg_hosts_file_entries view shows the entries of the current hosts configuration in either diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml index 971fb9fc453..0b0d0c648a9 100644 --- a/doc/src/sgml/system-views.sgml +++ b/doc/src/sgml/system-views.sgml @@ -1554,6 +1554,16 @@ AND c1.path[c2.level] = c2.path[c2.level]; + + + settings jsonb + + + Configuration parameters applied to connections matched by this + rule, as an object of parameter names and values, if any + + + error text @@ -1710,6 +1720,16 @@ AND c1.path[c2.level] = c2.path[c2.level]; + + + settings jsonb + + + Configuration parameters applied to the connections this entry is + selected for, if any + + + error text @@ -4120,13 +4140,43 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx + + + hosts + + + These settings behave like sighup settings, and can + also be set per entry of the SNI hosts configuration in JSON format + (see ); such a value applies to the + connections matched by that entry. Settings + with any of the context types listed below + can also be set per host entry. + + + + + + hba + + + These settings behave like sighup settings, and can + also be set per rule of the client authentication rules in JSON + format (see ), or per entry of the + SNI hosts configuration in JSON format; the value applies to the + connections matched by that rule or entry. Settings with any of + the context types listed below can also be + set per rule or host entry. + + + superuser-backend Changes to these settings can be made in - postgresql.conf without restarting the server. + postgresql.conf without restarting the server, + or per host entry or per client authentication rule. They can also be set for a particular session in the connection request packet (for example, via libpq's PGOPTIONS environment variable), but only if the connecting user is a superuser @@ -4145,7 +4195,8 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx Changes to these settings can be made in - postgresql.conf without restarting the server. + postgresql.conf without restarting the server, + or per host entry or per client authentication rule. They can also be set for a particular session in the connection request packet (for example, via libpq's PGOPTIONS environment variable); any user can make such a change for their session. @@ -4163,6 +4214,7 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx These settings can be set from postgresql.conf, + per host entry or per client authentication rule, or within a session via the SET command; but only superusers and users with the appropriate SET privilege can change them via SET. Changes in diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 12bf153d66f..6f332eee7ba 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -40,6 +40,8 @@ #include "storage/ipc.h" #include "tcop/backend_startup.h" #include "utils/memutils.h" +#include "utils/timeout.h" +#include "utils/timestamp.h" /*---------------------------------------------------------------- * Global authentication functions @@ -386,6 +388,7 @@ ClientAuthentication(Port *port) * STATUS_EOF. */ bool abandoned = false; + int global_timeout = AuthenticationTimeout; /* * Get the authentication method to use for this frontend/database @@ -395,6 +398,17 @@ ClientAuthentication(Port *port) */ hba_getauthmethod(port); + /* + * The matched rule may have changed authentication_timeout. The timer + * our caller armed used the global value; move its deadline, measured + * from the original start so that a per-rule value does not grant a fresh + * window. + */ + if (AuthenticationTimeout != global_timeout) + enable_timeout_at(STATEMENT_TIMEOUT, + TimestampTzPlusMilliseconds(get_timeout_start_time(STATEMENT_TIMEOUT), + AuthenticationTimeout * 1000)); + CHECK_FOR_INTERRUPTS(); /* diff --git a/src/backend/libpq/be-secure-common.c b/src/backend/libpq/be-secure-common.c index e6108f7641d..ad7defb97eb 100644 --- a/src/backend/libpq/be-secure-common.c +++ b/src/backend/libpq/be-secure-common.c @@ -530,3 +530,23 @@ load_hosts(List **hosts, char **err_msg) return HOSTSFILE_LOAD_OK; } + +/* + * apply_hosts_settings + * + * Apply the settings of the hosts entry chosen for a connection. This runs + * in the backend during the TLS handshake, before client authentication. + * The settings were validated when the entry was loaded, so a failure here + * is unexpected and only logged. An hba rule applies its settings later and + * from a higher source, so it overrides these on conflict. + */ +void +apply_hosts_settings(HostsLine *host) +{ + ListCell *k, + *v; + + forboth(k, host->settings_keys, v, host->settings_vals) + (void) set_config_option(lfirst(k), lfirst(v), PGC_HOSTS, PGC_S_HOSTS, + GUC_ACTION_SET, true, LOG, false); +} diff --git a/src/backend/libpq/be-secure-openssl.c b/src/backend/libpq/be-secure-openssl.c index 11f5a483b36..95163955872 100644 --- a/src/backend/libpq/be-secure-openssl.c +++ b/src/backend/libpq/be-secure-openssl.c @@ -2098,6 +2098,9 @@ sni_clienthello_cb(SSL *ssl, int *al, void *arg) } found: + /* the chosen entry may carry settings for this connection */ + apply_hosts_settings(install_config); + if (!ssl_update_ssl(ssl, install_config)) { *al = SSL_AD_INTERNAL_ERROR; diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index d00949c73c8..8ed673efc98 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -1383,6 +1383,8 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) ListCell *field; List *tokens; ListCell *tokencell; + ListCell *k, + *v; AuthToken *token; HbaLine *parsedline; @@ -1392,11 +1394,18 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) parsedline->rawline = pstrdup(tok_line->raw_line); /* - * A rule compiled from the JSON tree carries its node path outside the - * token stream; copy it into our context as well. + * A rule compiled from the JSON tree carries its node path and settings + * outside the token stream; copy them into our context as well. */ if (tok_line->node_path != NULL) parsedline->node_path = pstrdup(tok_line->node_path); + forboth(k, tok_line->settings_keys, v, tok_line->settings_vals) + { + parsedline->settings_keys = lappend(parsedline->settings_keys, + pstrdup(lfirst(k))); + parsedline->settings_vals = lappend(parsedline->settings_vals, + pstrdup(lfirst(v))); + } /* Check the record type. */ Assert(tok_line->fields != NIL); @@ -3059,6 +3068,22 @@ load_ident(void) +/* + * Apply the settings of the matched rule. They were validated when the + * rule was loaded, so a failure here is unexpected; log and go on, the + * connection is still usable. + */ +static void +apply_hba_settings(HbaLine *hba) +{ + ListCell *k, + *v; + + forboth(k, hba->settings_keys, v, hba->settings_vals) + (void) set_config_option(lfirst(k), lfirst(v), PGC_HBA, PGC_S_HBA, + GUC_ACTION_SET, true, LOG, false); +} + /* * Determine what authentication method should be used when accessing database * "database" from frontend "raddr", user "user". Return the method and @@ -3066,11 +3091,16 @@ load_ident(void) * * If the file does not contain any entry matching the request, we return * method = uaImplicitReject. + * + * The matched rule's settings take effect here, before authentication + * starts, so that they can influence it (authentication_timeout). The + * implicit reject rule has none. */ void hba_getauthmethod(Port *port) { check_hba(port); + apply_hba_settings(port->hba); } diff --git a/src/backend/libpq/hba_json.c b/src/backend/libpq/hba_json.c index 859c02f1ba5..71ee89ca358 100644 --- a/src/backend/libpq/hba_json.c +++ b/src/backend/libpq/hba_json.c @@ -5,7 +5,7 @@ * * The "hba" section of a JSON configuration is a tree of nodes. Each node * narrows the match (transport, ssl, gss, databases, users, address) and - * carries authentication options (auth) that its children inherit. We + * carries a property bag (auth, settings) that its children inherit. We * walk the tree depth first, children before the node's own rule, and emit * one TokenizedAuthLine per node that has an authentication method, with * the fields in the classic pg_hba.conf order. parse_hba_line() then @@ -36,6 +36,7 @@ * effective node by applying the child's own keys: * * - match fields replace the inherited value wholesale; + * - "settings" merge key by key; * - "auth" merges key by key, unless the child sets "method", in which * case the child's "auth" replaces the parent's entirely, since the * parent's options belong to a method that is no longer in use. @@ -47,14 +48,20 @@ * method emits nothing of its own; it only groups its children. * * Import nodes splice a legacy file in at their position. Legacy lines - * are complete rules and inherit nothing. The "import" lists of the - * ident and hosts sections work the same way. + * are complete rules, so they inherit only "settings", never match fields + * or "auth". The "import" lists of the ident and hosts sections work the + * same way, without anything to inherit. * * The "defaults" object of the hosts section is merged key by key across * all documents, later documents overriding, and then applied under every * entry's own keys. Host names must be unique across the documents and * the imported files, compared case-insensitively as the SNI match is. * + * Settings are not part of the token stream, since parse_hba_line() knows + * nothing about them. They are validated here, so that applying them at + * match time cannot fail, and attached to the TokenizedAuthLine, from + * where parse_hba_line() copies them into the HbaLine. + * *------------------------------------------------------------------------- */ #include "postgres.h" @@ -89,6 +96,8 @@ typedef struct HbaNodeState JsonConfNode *address; /* NULL = "all" */ List *auth_keys; /* char *, "method" included */ List *auth_vals; /* JsonConfNode * */ + List *settings_keys; /* char * */ + List *settings_vals; /* JsonConfNode * */ } HbaNodeState; static void walk_node(JsonConfNode *node, const HbaNodeState *parent, @@ -97,17 +106,20 @@ static void walk_node(JsonConfNode *node, const HbaNodeState *parent, /* * Replace or append a key in a pair of parallel lists. Insertion order is - * kept, so inherited keys stay in front of the node's own. + * kept, so inherited keys stay in front of the node's own. Setting names + * are parameter names, which match regardless of case; everything else + * matches exactly. */ static void -kv_put(List **keys, List **vals, const char *key, void *val) +kv_put(List **keys, List **vals, const char *key, void *val, bool guc_names) { ListCell *k, *v; forboth(k, *keys, v, *vals) { - if (strcmp(lfirst(k), key) == 0) + if (guc_names ? guc_name_compare(lfirst(k), key) == 0 : + strcmp(lfirst(k), key) == 0) { lfirst(v) = val; return; @@ -132,11 +144,39 @@ kv_get(List *keys, List *vals, const char *key) } /* - * A line that carries the node's identity, but no fields yet. raw_line is - * the node path until the caller knows the rule text. + * Build the text form of the effective settings, as parse_hba_line() and + * the GUC machinery want them. + */ +static void +settings_text_lists(const HbaNodeState *st, List **keys, List **vals) +{ + ListCell *k, + *v; + + *keys = NIL; + *vals = NIL; + forboth(k, st->settings_keys, v, st->settings_vals) + { + JsonConfNode *at; + char *err; + char *text; + + /* merge_node() has checked the shape */ + text = config_json_setting_text(lfirst(k), lfirst(v), &at, &err); + + Assert(text !=NULL); + *keys = lappend(*keys, pstrdup(lfirst(k))); + *vals = lappend(*vals, pstrdup(text)); + } +} + +/* + * A line that carries the node's identity and effective settings, but no + * fields yet. raw_line is the node path until the caller knows the rule + * text. "st" is NULL for an ident line, which has no settings. */ static TokenizedAuthLine * -new_tok_line(JsonConfNode *node) +new_tok_line(JsonConfNode *node, const HbaNodeState *st) { TokenizedAuthLine *tok = palloc0_object(TokenizedAuthLine); @@ -144,6 +184,8 @@ new_tok_line(JsonConfNode *node) tok->line_num = node->line; tok->node_path = config_json_path(node); tok->raw_line = pstrdup(tok->node_path); + if (st != NULL) + settings_text_lists(st, &tok->settings_keys, &tok->settings_vals); return tok; } @@ -166,14 +208,15 @@ node_error_callback(void *arg) * "node". The message is translated here. */ static void hba_node_error(JsonConfNode *node, JsonConfNode *at, - List **tok_lines, int elevel, - const char *fmt, ...) pg_attribute_printf(5, 6); + const HbaNodeState *st, List **tok_lines, + int elevel, const char *fmt, ...) + pg_attribute_printf(6, 7); static void -hba_node_error(JsonConfNode *node, JsonConfNode *at, List **tok_lines, - int elevel, const char *fmt, ...) +hba_node_error(JsonConfNode *node, JsonConfNode *at, const HbaNodeState *st, + List **tok_lines, int elevel, const char *fmt, ...) { - TokenizedAuthLine *tok = new_tok_line(node); + TokenizedAuthLine *tok = new_tok_line(node, st); StringInfoData msg; fmt = _(fmt); @@ -201,7 +244,7 @@ hba_node_error(JsonConfNode *node, JsonConfNode *at, List **tok_lines, /* The keys of an hba node other than an import node. */ static const char *const node_keys[] = { "transport", "ssl", "gss", "databases", "users", "address", "auth", - "children", NULL + "settings", "children", NULL }; /* Is the string one of the given keywords? The list ends with NULL. */ @@ -258,7 +301,7 @@ check_key_type(const char *key, JsonConfNode *val) return psprintf(_("\"%s\" must be a string or a non-empty array of strings"), key); } - else if (strcmp(key, "auth") == 0) + else if (strcmp(key, "auth") == 0 || strcmp(key, "settings") == 0) { if (val->type != JCN_OBJECT) return psprintf(_("\"%s\" must be an object"), key); @@ -289,11 +332,14 @@ merge_node(const HbaNodeState *parent, JsonConfNode *node, ListCell *k, *v; JsonConfNode *val, - *auth; + *auth, + *settings; *out = *parent; out->auth_keys = list_copy(parent->auth_keys); out->auth_vals = list_copy(parent->auth_vals); + out->settings_keys = list_copy(parent->settings_keys); + out->settings_vals = list_copy(parent->settings_vals); forboth(k, node->keys, v, node->values) { @@ -348,7 +394,22 @@ merge_node(const HbaNodeState *parent, JsonConfNode *node, (char *) lfirst(k)); return false; } - kv_put(&out->auth_keys, &out->auth_vals, lfirst(k), o); + kv_put(&out->auth_keys, &out->auth_vals, lfirst(k), o, false); + } + } + + /* settings always merge */ + settings = config_json_get(node, "settings"); + if (settings != NULL) + { + forboth(k, settings->keys, v, settings->values) + { + JsonConfNode *o = lfirst(v); + + /* a list value is joined as it is for the GUCs of the document */ + if (config_json_setting_text(lfirst(k), o, at, err) == NULL) + return false; + kv_put(&out->settings_keys, &out->settings_vals, lfirst(k), o, true); } } @@ -492,6 +553,27 @@ type_keyword(JsonConfNode *node, const HbaNodeState *st, char **err) return "host"; } +/* + * Validate the effective settings of a node that is about to emit rules. + * A grouping node without a method never gets here; a bad setting on it + * is reported by each emitting descendant, which inherits it. + */ +static bool +check_settings(JsonConfNode *node, const HbaNodeState *st, List **tok_lines, + int elevel) +{ + List *keys, + *vals; + char *err = NULL; + + settings_text_lists(st, &keys, &vals); + if (validate_connection_settings(keys, vals, PGC_HBA, &err)) + return true; + + hba_node_error(node, node, st, tok_lines, elevel, "%s", err); + return false; +} + /* * The classic text of a rule built from the given fields, for the "line * matched" messages: the JSON node alone does not show what it inherited. @@ -591,9 +673,11 @@ emit_rule(JsonConfNode *node, const HbaNodeState *st, List **tok_lines, if ((type = type_keyword(node, st, &err)) == NULL) { - hba_node_error(node, node, tok_lines, elevel, "%s", err); + hba_node_error(node, node, st, tok_lines, elevel, "%s", err); return; } + if (!check_settings(node, st, tok_lines, elevel)) + return; /* * The lists are shared by the emitted lines; parse_hba_line() copies the @@ -612,7 +696,7 @@ emit_rule(JsonConfNode *node, const HbaNodeState *st, List **tok_lines, if (err != NULL) { /* the @file error has been logged with the node's context already */ - TokenizedAuthLine *tok = new_tok_line(node); + TokenizedAuthLine *tok = new_tok_line(node, st); tok->err_msg = err; *tok_lines = lappend(*tok_lines, tok); @@ -630,7 +714,7 @@ emit_rule(JsonConfNode *node, const HbaNodeState *st, List **tok_lines, foreach(lc, addresses) { AuthToken *addr = lfirst(lc); - TokenizedAuthLine *tok = new_tok_line(node); + TokenizedAuthLine *tok = new_tok_line(node, st); List *fields; ListCell *k, *v; @@ -735,16 +819,18 @@ read_import_file(JsonConfNode *fileval, bool optional, List **lines, /* * Splice a legacy file in at the import node's position. The legacy lines - * are complete rules and inherit nothing. + * inherit the effective settings, nothing else. */ static void -import_node(JsonConfNode *node, List **tok_lines, int elevel) +import_node(JsonConfNode *node, const HbaNodeState *st, List **tok_lines, + int elevel) { JsonConfNode *imp = config_json_get(node, "import"); JsonConfNode *opt = config_json_get(node, "optional"); ListCell *k, *v; List *lines; + ListCell *lc; char *err; /* an import node is a leaf with exactly these two keys */ @@ -755,31 +841,34 @@ import_node(JsonConfNode *node, List **tok_lines, int elevel) if (strcmp(key, "import") == 0 || strcmp(key, "optional") == 0) continue; if (string_in(key, node_keys)) - hba_node_error(node, lfirst(v), tok_lines, elevel, + hba_node_error(node, lfirst(v), st, tok_lines, elevel, "import node cannot have \"%s\"", key); else - hba_node_error(node, lfirst(v), tok_lines, elevel, + hba_node_error(node, lfirst(v), st, tok_lines, elevel, "unrecognized key \"%s\"", key); return; } if (imp->type != JCN_STRING) { - hba_node_error(node, imp, tok_lines, elevel, + hba_node_error(node, imp, st, tok_lines, elevel, "\"%s\" must be a string", "import"); return; } if (opt != NULL && opt->type != JCN_BOOL) { - hba_node_error(node, opt, tok_lines, elevel, + hba_node_error(node, opt, st, tok_lines, elevel, "\"%s\" must be a boolean", "optional"); return; } + if (!check_settings(node, st, tok_lines, elevel)) + return; + if (!read_import_file(imp, opt != NULL && opt->boolval, &lines, elevel, &err)) { if (err != NULL) { - TokenizedAuthLine *tok = new_tok_line(node); + TokenizedAuthLine *tok = new_tok_line(node, st); tok->err_msg = err; *tok_lines = lappend(*tok_lines, tok); @@ -787,7 +876,13 @@ import_node(JsonConfNode *node, List **tok_lines, int elevel) return; } - *tok_lines = list_concat(*tok_lines, lines); + foreach(lc, lines) + { + TokenizedAuthLine *tok = lfirst(lc); + + settings_text_lists(st, &tok->settings_keys, &tok->settings_vals); + *tok_lines = lappend(*tok_lines, tok); + } } /* @@ -808,7 +903,7 @@ walk_node(JsonConfNode *node, const HbaNodeState *parent, List **tok_lines, if (node->type != JCN_OBJECT) { - hba_node_error(node, node, tok_lines, elevel, + hba_node_error(node, node, parent, tok_lines, elevel, "hba node must be an object, not %s", config_json_type_name(node->type)); return; @@ -816,13 +911,13 @@ walk_node(JsonConfNode *node, const HbaNodeState *parent, List **tok_lines, if (config_json_get(node, "import") != NULL) { - import_node(node, tok_lines, elevel); + import_node(node, parent, tok_lines, elevel); return; } if (!merge_node(parent, node, &st, &err, &at)) { - hba_node_error(node, at, tok_lines, elevel, "%s", err); + hba_node_error(node, at, parent, tok_lines, elevel, "%s", err); return; } @@ -846,7 +941,7 @@ walk_node(JsonConfNode *node, const HbaNodeState *parent, List **tok_lines, * emits nothing and is almost certainly a mistake. An empty children * array is a leaf. */ - hba_node_error(node, node, tok_lines, elevel, + hba_node_error(node, node, &st, tok_lines, elevel, "leaf node has no authentication method"); } } @@ -958,7 +1053,7 @@ import_auth_lines(JsonConfNode *imports, List **tok_lines, int elevel, { if (err != NULL) { - TokenizedAuthLine *tok = new_tok_line(fileval); + TokenizedAuthLine *tok = new_tok_line(fileval, NULL); tok->err_msg = err; *tok_lines = lappend(*tok_lines, tok); @@ -1019,12 +1114,12 @@ emit_mapping(const char *mapname, const char *sysname, JsonConfNode *pg, if (pg->type != JCN_STRING) { - hba_node_error(pg, pg, tok_lines, elevel, + hba_node_error(pg, pg, NULL, tok_lines, elevel, "\"%s\" must be a string or a non-empty array of strings", sysname); return; } - tok = new_tok_line(pg); + tok = new_tok_line(pg, NULL); systoks = append_json_tokens(NIL, sysname, pg, elevel, &err); if (err == NULL) pgtoks = append_json_tokens(NIL, pg->strval, pg, elevel, &err); @@ -1128,12 +1223,12 @@ compile_ident_documents(List *documents, List **tok_lines, int elevel, } /* - * Keys of a hosts entry, and of "defaults": the ssl_* parameters an entry - * overrides for its host. + * Keys of a hosts entry, and of "defaults". All but "settings" are the + * ssl_* parameters an entry overrides for its host. */ static const char *const hosts_entry_keys[] = { "ssl_cert_file", "ssl_key_file", "ssl_ca_file", "ssl_passphrase_command", - "ssl_passphrase_command_supports_reload", NULL + "ssl_passphrase_command_supports_reload", "settings", NULL }; /* @@ -1215,6 +1310,30 @@ hosts_collect(JsonConfNode *obj, HostsLine *out, char **err, } out->ssl_passphrase_reload = val->boolval; } + else if (strcmp(key, "settings") == 0) + { + ListCell *sk, + *sv; + + if (val->type != JCN_OBJECT) + { + *err = psprintf(_("\"%s\" must be an object"), key); + return false; + } + forboth(sk, val->keys, sv, val->values) + { + JsonConfNode *o = lfirst(sv); + char *text; + + /* as in the hba section, an array is a list value */ + text = config_json_setting_text(lfirst(sk), o, at, err); + + if (text == NULL) + return false; + kv_put(&out->settings_keys, &out->settings_vals, lfirst(sk), + pstrdup(text), true); + } + } else { char **field; @@ -1241,8 +1360,9 @@ hosts_collect(JsonConfNode *obj, HostsLine *out, char **err, /* * Compile one key of "entries" into an entry. The entry starts out as a * copy of the merged defaults, its own keys go on top, and then the - * certificate and key must be there. The entry is returned even on - * error, with err_msg set. + * certificate and key must be there, and the settings must validate, so + * that applying them at the handshake cannot fail. The entry is returned + * even on error, with err_msg set. */ static HostsLine * compile_hosts_entry(const char *hostname, JsonConfNode *node, @@ -1277,6 +1397,8 @@ compile_hosts_entry(const char *hostname, JsonConfNode *node, entry->ssl_ca = defaults->ssl_ca; entry->ssl_passphrase_cmd = defaults->ssl_passphrase_cmd; entry->ssl_passphrase_reload = defaults->ssl_passphrase_reload; + entry->settings_keys = list_copy(defaults->settings_keys); + entry->settings_vals = list_copy(defaults->settings_vals); if (!hosts_collect(node, entry, &err, &at)) { hosts_entry_error(entry, at, elevel, "%s", err); @@ -1292,6 +1414,11 @@ compile_hosts_entry(const char *hostname, JsonConfNode *node, return entry; } + if (!validate_connection_settings(entry->settings_keys, + entry->settings_vals, + PGC_HOSTS, &err)) + hosts_entry_error(entry, node, elevel, "%s", err); + return entry; } @@ -1499,6 +1626,10 @@ copy_hosts_entry(const HostsLine *src) dst->ssl_passphrase_reload = src->ssl_passphrase_reload; if (src->node_path != NULL) dst->node_path = pstrdup(src->node_path); + foreach(lc, src->settings_keys) + dst->settings_keys = lappend(dst->settings_keys, pstrdup(lfirst(lc))); + foreach(lc, src->settings_vals) + dst->settings_vals = lappend(dst->settings_vals, pstrdup(lfirst(lc))); if (src->err_msg != NULL) dst->err_msg = pstrdup(src->err_msg); return dst; diff --git a/src/backend/nls.mk b/src/backend/nls.mk index f90012111bf..0e45465fd4e 100644 --- a/src/backend/nls.mk +++ b/src/backend/nls.mk @@ -18,7 +18,7 @@ GETTEXT_TRIGGERS = $(BACKEND_COMMON_GETTEXT_TRIGGERS) \ json_manifest_parse_failure:2 \ error_cb:2 \ json_setting_error:5 \ - hba_node_error:5 \ + hba_node_error:6 \ section_shape_error:4 \ hosts_entry_error:4 \ hosts_line_error:3 @@ -32,7 +32,7 @@ GETTEXT_FLAGS = $(BACKEND_COMMON_GETTEXT_FLAGS) \ json_token_error:2:c-format \ error_cb:2:c-format \ json_setting_error:5:c-format \ - hba_node_error:5:c-format \ + hba_node_error:6:c-format \ section_shape_error:4:c-format \ hosts_entry_error:4:c-format \ hosts_line_error:3:c-format diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c index a588fe53ea5..102db22f85d 100644 --- a/src/backend/utils/adt/hbafuncs.c +++ b/src/backend/utils/adt/hbafuncs.c @@ -25,7 +25,9 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/config_json.h" +#include "utils/fmgrprotos.h" #include "utils/guc.h" +#include "utils/json.h" #include "utils/tuplestore.h" @@ -173,8 +175,35 @@ get_hba_options(HbaLine *hba) return NULL; } +/* + * The settings attached to a rule or a hosts entry, as a jsonb object of + * name/value pairs; the values are the text they were given as. + */ +static Datum +settings_to_jsonb(List *keys, List *vals) +{ + StringInfoData buf; + ListCell *k, + *v; + bool first = true; + + initStringInfo(&buf); + appendStringInfoChar(&buf, '{'); + forboth(k, keys, v, vals) + { + if (!first) + appendStringInfoString(&buf, ", "); + first = false; + escape_json(&buf, lfirst(k)); + appendStringInfoString(&buf, ": "); + escape_json(&buf, lfirst(v)); + } + appendStringInfoChar(&buf, '}'); + return DirectFunctionCall1(jsonb_in, CStringGetDatum(buf.data)); +} + /* Number of columns in pg_hba_file_rules view */ -#define NUM_PG_HBA_FILE_RULES_ATTS 12 +#define NUM_PG_HBA_FILE_RULES_ATTS 13 /* * fill_hba_line @@ -366,8 +395,8 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, else { /* no parsing result, so set the fields derived from it to nulls */ - memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 5) * sizeof(bool)); - index = NUM_PG_HBA_FILE_RULES_ATTS - 2; + memset(&nulls[3], true, (NUM_PG_HBA_FILE_RULES_ATTS - 6) * sizeof(bool)); + index = NUM_PG_HBA_FILE_RULES_ATTS - 3; } /* node_path, known even when the rule did not parse */ @@ -376,6 +405,13 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, else nulls[index++] = true; + /* settings */ + if (hba != NULL && hba->settings_keys != NIL) + values[index++] = settings_to_jsonb(hba->settings_keys, + hba->settings_vals); + else + nulls[index++] = true; + /* error */ Assert(index == NUM_PG_HBA_FILE_RULES_ATTS - 1); if (err_msg) @@ -683,7 +719,7 @@ pg_ident_file_mappings(PG_FUNCTION_ARGS) } /* Number of columns in pg_hosts_file_entries view */ -#define NUM_PG_HOSTS_FILE_ENTRIES_ATTS 10 +#define NUM_PG_HOSTS_FILE_ENTRIES_ATTS 11 /* * fill_hosts_entry @@ -751,6 +787,11 @@ fill_hosts_entry(Tuplestorestate *tuple_store, TupleDesc tupdesc, else nulls[index++] = true; values[index++] = BoolGetDatum(host->ssl_passphrase_reload); + if (host->settings_keys != NIL) + values[index++] = settings_to_jsonb(host->settings_keys, + host->settings_vals); + else + nulls[index++] = true; } else { diff --git a/src/backend/utils/misc/config_json.c b/src/backend/utils/misc/config_json.c index ec5068dc74d..e5117b06d1f 100644 --- a/src/backend/utils/misc/config_json.c +++ b/src/backend/utils/misc/config_json.c @@ -578,6 +578,103 @@ json_file_error(const char *file, int line, int elevel, const char *msg, record_config_file_error(text, file, line, head_p, tail_p); } +/* + * Does this element of a GUC_LIST_QUOTE list need double quotes? Anything + * but lowercase ASCII letters, digits and underscores gets them, so that + * the element survives SplitGUCList unchanged. + */ +static bool +list_element_needs_quotes(const char *elem) +{ + if (elem[0] == '\0') + return true; + for (const char *p = elem; *p != '\0'; p++) + { + if (!((*p >= 'a' && *p <= 'z') || (*p >= '0' && *p <= '9') || + *p == '_')) + return true; + } + return false; +} + +/* + * config_json_setting_text + * The text of a setting given as JSON, as postgresql.conf would have it. + * + * A scalar is its text (config_json_scalar_text()). An array becomes a + * comma-separated list, which is how list parameters are written in + * postgresql.conf. For a GUC_LIST_QUOTE parameter the elements are quoted + * as needed, so that an element may contain any character. Other lists + * have no quoting convention, so an element containing a comma would + * silently turn into two; refuse it instead. + * + * Returns NULL for a value of the wrong shape, with *err_msg set to a + * translated message and *err_at to the offending node. + */ +char * +config_json_setting_text(const char *name, JsonConfNode *node, + JsonConfNode **err_at, char **err_msg) +{ + *err_at = NULL; + *err_msg = NULL; + if (node->type == JCN_ARRAY) + { + StringInfoData buf; + ListCell *lc; + struct config_generic *record; + bool quote; + + record = find_option(name, false, true, DEBUG5); + quote = (record != NULL && (record->flags & GUC_LIST_QUOTE) != 0); + + initStringInfo(&buf); + foreach(lc, node->values) + { + JsonConfNode *el = lfirst(lc); + char *t = config_json_scalar_text(el); + + if (t == NULL) + { + *err_at = el; + *err_msg = psprintf(_("\"%s\" must be a string, number, boolean, or array of these"), + name); + return NULL; + } + if (buf.len > 0) + appendStringInfoString(&buf, ", "); + if (quote && list_element_needs_quotes(t)) + { + appendStringInfoChar(&buf, '"'); + for (const char *p = t; *p != '\0'; p++) + { + if (*p == '"') + appendStringInfoChar(&buf, '"'); + appendStringInfoChar(&buf, *p); + } + appendStringInfoChar(&buf, '"'); + } + else if (!quote && strchr(t, ',') != NULL) + { + *err_at = el; + *err_msg = psprintf(_("elements of \"%s\" must not contain commas"), + name); + return NULL; + } + else + appendStringInfoString(&buf, t); + } + return buf.data; + } + if (node->type == JCN_OBJECT || node->type == JCN_NULL) + { + *err_at = node; + *err_msg = psprintf(_("\"%s\" must be a string, number, boolean, or array of these"), + name); + return NULL; + } + return config_json_scalar_text(node); +} + /* * Process one include entry: a string, or an object with "file" and an * optional "optional" flag. The file name may carry wildcards in its last diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c index 21c9ccec1e2..6f9e95f3534 100644 --- a/src/backend/utils/misc/guc.c +++ b/src/backend/utils/misc/guc.c @@ -3220,6 +3220,84 @@ parse_and_validate_value(const struct config_generic *record, } +/* + * validate_connection_settings + * Check settings attached to a client authentication rule before they + * are stored with it. + * + * Every entry of the parallel lists must name an existing parameter that + * can be set in the given context, with a value that parses. Done at load + * time, so that applying the settings when a connection matches cannot + * fail. + * + * The settings are applied before a database is selected, where a check + * hook cannot look up catalog objects and accepts their names on faith. + * The validation may however run in a connected backend, when the view + * over the file reloads it, where the same hook would reject a name it + * does not find. Validate as PGC_S_TEST, the source used for ALTER ROLE + * SET and other settings stored for later use: a hook then reports a + * missing object as a NOTICE at most, so the answer is the same wherever + * the file is loaded. The role is the bootstrap superuser's, as it is for + * any other configuration file; set_config_option() would take it from + * the current user, which the postmaster does not have. + * + * Returns false with *err_msg set to the first problem. Nothing is + * logged; the caller knows where the setting came from and reports it. + * set_config_option() has the message text we want (unrecognized + * parameter, cannot be changed now, invalid value), but only as an + * ereport, so run it at ERROR and catch the error. + */ +bool +validate_connection_settings(List *keys, List *vals, GucContext context, + char **err_msg) +{ + ListCell *k, + *v; + + *err_msg = NULL; + forboth(k, keys, v, vals) + { + MemoryContext oldcxt = CurrentMemoryContext; + bool ok = true; + + PG_TRY(); + { + /* + * With changeVal false a valid setting yields -1, "accepted but + * not applied". A problem is reported at ERROR and lands in the + * catch block, so 0 cannot be returned here; check it anyway in + * case that ever changes. + */ + if (set_config_option_ext(lfirst(k), lfirst(v), context, + PGC_S_TEST, BOOTSTRAP_SUPERUSERID, + GUC_ACTION_SET, false, ERROR, + false) == 0) + ok = false; + } + PG_CATCH(); + { + ErrorData *edata; + + MemoryContextSwitchTo(oldcxt); + edata = CopyErrorData(); + FlushErrorState(); + *err_msg = pstrdup(edata->message); + FreeErrorData(edata); + ok = false; + } + PG_END_TRY(); + + if (!ok) + { + if (*err_msg == NULL) + *err_msg = psprintf("parameter \"%s\" cannot be set here", + (char *) lfirst(k)); + return false; + } + } + return true; +} + /* * set_config_option: sets option `name' to given value. * @@ -3449,6 +3527,28 @@ set_config_with_handle(const char *name, config_handle *handle, * signals to individual backends only. */ break; + case PGC_HOSTS: + if (context != PGC_HOSTS && context != PGC_SIGHUP && + context != PGC_POSTMASTER) + { + ereport(elevel, + (errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM), + errmsg("parameter \"%s\" cannot be changed now", + record->name))); + return 0; + } + break; + case PGC_HBA: + if (context != PGC_HBA && context != PGC_HOSTS && + context != PGC_SIGHUP && context != PGC_POSTMASTER) + { + ereport(elevel, + (errcode(ERRCODE_CANT_CHANGE_RUNTIME_PARAM), + errmsg("parameter \"%s\" cannot be changed now", + record->name))); + return 0; + } + break; case PGC_SU_BACKEND: if (context == PGC_BACKEND) { @@ -3502,6 +3602,8 @@ set_config_with_handle(const char *name, config_handle *handle, else if (context != PGC_POSTMASTER && context != PGC_BACKEND && context != PGC_SU_BACKEND && + context != PGC_HOSTS && + context != PGC_HBA && source != PGC_S_CLIENT) { ereport(elevel, diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat index 380679a76c0..eb04e482058 100644 --- a/src/backend/utils/misc/guc_parameters.dat +++ b/src/backend/utils/misc/guc_parameters.dat @@ -105,7 +105,7 @@ max => 'INT_MAX / 2', }, -{ name => 'authentication_timeout', type => 'int', context => 'PGC_SIGHUP', group => 'CONN_AUTH_AUTH', +{ name => 'authentication_timeout', type => 'int', context => 'PGC_HBA', group => 'CONN_AUTH_AUTH', short_desc => 'Sets the maximum allowed time to complete client authentication.', flags => 'GUC_UNIT_S', variable => 'AuthenticationTimeout', diff --git a/src/backend/utils/misc/guc_tables.c b/src/backend/utils/misc/guc_tables.c index 342aaeef59a..8674c006689 100644 --- a/src/backend/utils/misc/guc_tables.c +++ b/src/backend/utils/misc/guc_tables.c @@ -696,6 +696,8 @@ const char *const GucContext_Names[] = [PGC_INTERNAL] = "internal", [PGC_POSTMASTER] = "postmaster", [PGC_SIGHUP] = "sighup", + [PGC_HOSTS] = "hosts", + [PGC_HBA] = "hba", [PGC_SU_BACKEND] = "superuser-backend", [PGC_BACKEND] = "backend", [PGC_SUSET] = "superuser", @@ -718,6 +720,8 @@ const char *const GucSource_Names[] = [PGC_S_FILE] = "configuration file", [PGC_S_ARGV] = "command line", [PGC_S_GLOBAL] = "global", + [PGC_S_HOSTS] = "hosts", + [PGC_S_HBA] = "hba", [PGC_S_DATABASE] = "database", [PGC_S_USER] = "user", [PGC_S_DATABASE_USER] = "database user", diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index e2aed4fa027..cc1f6ef7a55 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -6672,9 +6672,9 @@ { oid => '3401', descr => 'show client authentication rules', proname => 'pg_hba_file_rules', prorows => '1000', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', - proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text,text}', - proargmodes => '{o,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,node_path,error}', + proallargtypes => '{int4,text,int4,text,_text,_text,text,text,text,_text,text,jsonb,text}', + proargmodes => '{o,o,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,node_path,settings,error}', prosrc => 'pg_hba_file_rules', proacl => '{POSTGRES=X}' }, { oid => '6250', descr => 'show user name mappings', proname => 'pg_ident_file_mappings', prorows => '1000', proretset => 't', @@ -6686,9 +6686,9 @@ { oid => '8801', descr => 'show SNI host entries', proname => 'pg_hosts_file_entries', prorows => '100', proretset => 't', provolatile => 'v', prorettype => 'record', proargtypes => '', - proallargtypes => '{int4,text,int4,text,text,text,text,text,bool,text}', - proargmodes => '{o,o,o,o,o,o,o,o,o,o}', - proargnames => '{entry_number,file_name,line_number,hostname,ssl_cert_file,ssl_key_file,ssl_ca_file,ssl_passphrase_command,ssl_passphrase_command_supports_reload,error}', + proallargtypes => '{int4,text,int4,text,text,text,text,text,bool,jsonb,text}', + proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{entry_number,file_name,line_number,hostname,ssl_cert_file,ssl_key_file,ssl_ca_file,ssl_passphrase_command,ssl_passphrase_command_supports_reload,settings,error}', prosrc => 'pg_hosts_file_entries', proacl => '{POSTGRES=X}' }, { 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 cf77b8447a0..99452df98e0 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -135,6 +135,8 @@ typedef struct HbaLine List *oauth_opt_vals; char *node_path; /* JSON node this rule was compiled from, or * NULL for a legacy line */ + List *settings_keys; /* parameters applied when the rule matches */ + List *settings_vals; } HbaLine; typedef struct IdentLine @@ -165,6 +167,8 @@ typedef struct HostsLine char *node_path; /* JSON node this entry was compiled from, or * NULL for a legacy line */ + List *settings_keys; /* parameters applied when the entry matches */ + List *settings_vals; char *err_msg; /* why the entry is unusable, or NULL */ /* Internal bookkeeping */ @@ -189,6 +193,8 @@ typedef struct TokenizedAuthLine char *err_msg; /* Error message if any */ char *node_path; /* JSON node this rule was compiled from, or * NULL for a legacy line */ + List *settings_keys; /* per-rule settings from the JSON node */ + List *settings_vals; } TokenizedAuthLine; /* avoid including libpq/libpq-be.h here */ diff --git a/src/include/libpq/libpq.h b/src/include/libpq/libpq.h index d15073a0a93..49e4ae59449 100644 --- a/src/include/libpq/libpq.h +++ b/src/include/libpq/libpq.h @@ -174,5 +174,6 @@ extern int run_ssl_passphrase_command(const char *cmd, const char *prompt, extern bool check_ssl_key_file_permissions(const char *ssl_key_file, bool isServerStart); extern HostsFileLoadResult load_hosts(List **hosts, char **err_msg); +extern void apply_hosts_settings(HostsLine *host); #endif /* LIBPQ_H */ diff --git a/src/include/utils/config_json.h b/src/include/utils/config_json.h index ffdfab1b896..152e68521b6 100644 --- a/src/include/utils/config_json.h +++ b/src/include/utils/config_json.h @@ -59,5 +59,7 @@ extern int config_json_errcontext(JsonConfNode *node); extern char *config_json_error_text(JsonConfNode *node, const char *msg); extern const char *config_json_type_name(JsonConfNodeType type); extern char *config_json_scalar_text(JsonConfNode *node); +extern char *config_json_setting_text(const char *name, JsonConfNode *node, + JsonConfNode **err_at, char **err_msg); #endif /* CONFIG_JSON_H */ diff --git a/src/include/utils/guc.h b/src/include/utils/guc.h index 164efba6b51..3704556fb46 100644 --- a/src/include/utils/guc.h +++ b/src/include/utils/guc.h @@ -54,14 +54,24 @@ * certain point in their main loop. It's safer to wait than to read a * file asynchronously.) * + * HOSTS options can be set everywhere SIGHUP options can, and additionally + * for the connections matched by an entry of the hosts configuration. + * + * HBA options can be set everywhere HOSTS options can, and additionally + * for the connections matched by a rule of the client authentication + * configuration. Both are applied as soon as the connection has been + * matched, before authentication, and remain in effect for the life of + * the connection. Options of any later (less restrictive) context can + * also be set from a hosts entry or an hba rule. + * * BACKEND and SU_BACKEND options can only be set at postmaster startup, - * from the configuration file, or by client request in the connection - * startup packet (e.g., from libpq's PGOPTIONS variable). SU_BACKEND - * options can be set from the startup packet only when the user is a - * superuser. Furthermore, an already-started backend will ignore changes - * to such an option in the configuration file. The idea is that these - * options are fixed for a given backend once it's started, but they can - * vary across backends. + * from the configuration file, from a hosts entry or an hba rule, or by + * client request in the connection startup packet (e.g., from libpq's + * PGOPTIONS variable). SU_BACKEND options can be set from the startup + * packet only when the user is a superuser. Furthermore, an + * already-started backend will ignore changes to such an option in the + * configuration file. The idea is that these options are fixed for a + * given backend once it's started, but they can vary across backends. * * SUSET options can be set at postmaster startup, with the SIGHUP * mechanism, or from the startup packet or SQL if you're a superuser. @@ -73,6 +83,8 @@ typedef enum PGC_INTERNAL, PGC_POSTMASTER, PGC_SIGHUP, + PGC_HOSTS, /* also settable per hosts entry */ + PGC_HBA, /* also settable per hba rule */ PGC_SU_BACKEND, PGC_BACKEND, PGC_SUSET, @@ -88,6 +100,11 @@ typedef enum * Sources <= PGC_S_OVERRIDE will set the default used by RESET, as well * as the current value. * + * PGC_S_HOSTS and PGC_S_HBA rank above PGC_S_GLOBAL and below + * PGC_S_DATABASE so that a per-database or per-role setting still + * overrides a value from a hosts entry or hba rule, while global + * defaults do not. + * * PGC_S_INTERACTIVE isn't actually a source value, but is the * dividing line between "interactive" and "non-interactive" sources for * error reporting purposes. @@ -116,6 +133,8 @@ typedef enum PGC_S_FILE, /* postgresql.conf */ PGC_S_ARGV, /* postmaster command line */ PGC_S_GLOBAL, /* global in-database setting */ + PGC_S_HOSTS, /* from the matched hosts entry */ + PGC_S_HBA, /* from the matched hba rule */ PGC_S_DATABASE, /* per-database setting */ PGC_S_USER, /* per-user setting */ PGC_S_DATABASE_USER, /* per-user-and-database setting */ @@ -448,6 +467,9 @@ extern int set_config_option(const char *name, const char *value, GucContext context, GucSource source, GucAction action, bool changeVal, int elevel, bool is_reload); +extern bool validate_connection_settings(List *keys, List *vals, + GucContext context, char **err_msg); +extern int guc_name_compare(const char *namea, const char *nameb); extern int set_config_option_ext(const char *name, const char *value, GucContext context, GucSource source, Oid srole, diff --git a/src/test/authentication/t/008_json_hba.pl b/src/test/authentication/t/008_json_hba.pl index 9d22151bf3c..e8d72dd817a 100644 --- a/src/test/authentication/t/008_json_hba.pl +++ b/src/test/authentication/t/008_json_hba.pl @@ -69,16 +69,16 @@ $node->safe_psql( # The three-level example from the proposal, with scram standing in for # LDAP so it can be tested without an LDAP server: everyone scram, -# admins additionally need a client certificate, jacob is rejected. -# clientcert needs an SSL rule, which this cluster cannot match, so the -# admins get a different method instead. +# admins additionally get a per-rule setting, jacob is rejected. set_hba(<<'EOF'); {"children": [ {"transport": "local", "auth": {"method": "trust"}}, {"address": "127.0.0.1/32", "auth": {"method": "scram-sha-256"}, + "settings": {"work_mem": "11MB"}, "children": [ - {"users": "+admins", "auth": {"method": "trust"}}, + {"users": "+admins", + "settings": {"work_mem": "12MB", "log_connections": "all"}}, {"users": "jacob", "auth": {"method": "reject"}} ]} ]} @@ -88,26 +88,95 @@ $node->reload; my $res = $node->safe_psql( 'postgres', q{ SELECT rule_number, type, user_name, auth_method, node_path, - error IS NULL + settings::text, error IS NULL FROM pg_hba_file_rules ORDER BY rule_number}); is( $res, join( "\n", - '1|local|{all}|trust|$.hba.children[0]|t', - '2|host|{+admins}|trust|$.hba.children[1].children[0]|t', - '3|host|{jacob}|reject|$.hba.children[1].children[1]|t', - '4|host|{all}|scram-sha-256|$.hba.children[1]|t'), - 'tree compiled depth-first, children before own rule'); + '1|local|{all}|trust|$.hba.children[0]||t', + '2|host|{+admins}|scram-sha-256|$.hba.children[1].children[0]|' + . '{"work_mem": "12MB", "log_connections": "all"}|t', + '3|host|{jacob}|reject|$.hba.children[1].children[1]|' + . '{"work_mem": "11MB"}|t', + '4|host|{all}|scram-sha-256|$.hba.children[1]|' + . '{"work_mem": "11MB"}|t'), + 'tree compiled depth-first, children before own rule, settings merged'); $ENV{PGPASSWORD} = 'pass'; -$node->connect_ok('host=127.0.0.1 dbname=postgres user=alice', - 'everyone else authenticates with scram'); -$node->connect_ok('host=127.0.0.1 dbname=postgres user=bob', - 'admin matched by the more specific child'); +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=alice', + 'everyone else authenticates with scram', + sql => 'SHOW work_mem', + expected_stdout => qr/^11MB$/m); +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=bob', + 'admin inherits method, overrides setting', + sql => 'SHOW work_mem', + expected_stdout => qr/^12MB$/m); $node->connect_fails( 'host=127.0.0.1 dbname=postgres user=jacob', 'jacob is rejected by the more specific child', expected_stderr => qr/rejects connection/); +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=alice', + 'hba settings show source hba', + sql => q{SELECT source FROM pg_settings WHERE name = 'work_mem'}, + expected_stdout => qr/^hba$/m); + +# per-role settings rank above hba settings +$node->safe_psql('postgres', "ALTER ROLE alice SET work_mem = '21MB'"); +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=alice', + 'role setting overrides hba setting', + sql => 'SHOW work_mem', + expected_stdout => qr/^21MB$/m); +$node->safe_psql('postgres', 'ALTER ROLE alice RESET work_mem'); + +# setting names match regardless of case when merged, as parameters do +set_hba(<<'EOF'); +{"children": [ + {"transport": "local", "auth": {"method": "trust"}}, + {"address": "127.0.0.1/32", "auth": {"method": "scram-sha-256"}, + "settings": {"work_mem": "1MB"}, + "children": [ + {"settings": {"Work_Mem": "2MB"}, "children": [ + {"users": "alice", "settings": {"work_mem": "3MB"}} + ]} + ]} +]} +EOF +$node->reload; +$res = $node->safe_psql( + 'postgres', q{ + SELECT settings::text FROM pg_hba_file_rules WHERE rule_number = 2}); +is($res, '{"work_mem": "3MB"}', 'setting overridden regardless of case'); +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=alice', + 'deepest override of a setting wins', + sql => 'SHOW work_mem', + expected_stdout => qr/^3MB$/m); + +# a value naming a database object that does not exist is accepted, as it +# is for ALTER ROLE SET; the view, loading the file in a connected backend, +# agrees with the postmaster, which cannot look the name up +set_hba(<<'EOF'); +{"children": [ + {"transport": "local", "auth": {"method": "trust"}}, + {"address": "127.0.0.1/32", "auth": {"method": "scram-sha-256"}, + "settings": {"default_tablespace": "no_such_space"}} +]} +EOF +$node->reload; +$res = $node->safe_psql( + 'postgres', q{ + SET client_min_messages = warning; + SELECT error IS NULL FROM pg_hba_file_rules WHERE rule_number = 2}); +is($res, 't', 'missing tablespace accepted by the view as by the load'); +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=alice', + 'missing tablespace applied on faith', + sql => 'SHOW default_tablespace', + expected_stdout => qr/^no_such_space$/m); # The example of the documentation, with scram for LDAP: everyone over # SSL, admins additionally need a client certificate, jacob is rejected. @@ -170,14 +239,14 @@ $res = $node->safe_psql( is($res, qq{2|127.0.0.1|\$.hba.children[1]\n3|::1|\$.hba.children[1]}, 'address array expands to one rule per element'); -# positional import of a legacy file, inside a grouping node; a missing +# positional import of a legacy file, inheriting settings only; a missing # optional import is skipped write_file("$datadir/legacy_hba.conf", "host all alice 127.0.0.1/32 trust\n"); set_hba(<<'EOF'); {"children": [ {"transport": "local", "auth": {"method": "trust"}}, - {"children": [ + {"settings": {"work_mem": "13MB"}, "children": [ {"import": "missing.conf", "optional": true}, {"import": "legacy_hba.conf"}, {"address": "127.0.0.1/32", "auth": {"method": "reject"}} @@ -188,16 +257,24 @@ $node->reload; $res = $node->safe_psql( 'postgres', q{ SELECT rule_number, regexp_replace(file_name, '.*/', ''), line_number, - user_name, auth_method, node_path IS NULL + user_name, auth_method, node_path IS NULL, settings::text FROM pg_hba_file_rules WHERE rule_number > 1 ORDER BY rule_number}); is( $res, join( "\n", - '2|legacy_hba.conf|1|{alice}|trust|t', - '3|pg_hba.json|6|{all}|reject|f'), - 'import inserts legacy rules at its position'); -$node->connect_ok('host=127.0.0.1 dbname=postgres user=alice', - 'legacy rule matched before the sibling after it'); + '2|legacy_hba.conf|1|{alice}|trust|t|{"work_mem": "13MB"}', + '3|pg_hba.json|6|{all}|reject|f|{"work_mem": "13MB"}'), + 'import inserts legacy rules at its position with inherited settings'); +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=alice', + 'legacy rule matched before the sibling after it', + sql => 'SHOW work_mem', + expected_stdout => qr/^13MB$/m); +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=alice', + 'setting inherited by a legacy rule shows source hba', + sql => q{SELECT source FROM pg_settings WHERE name = 'work_mem'}, + expected_stdout => qr/^hba$/m); # an included document adds its rules after the including one; the # include is relative to the document @@ -331,6 +408,36 @@ $node->connect_fails( 'quoted user token is not the keyword', expected_stderr => qr/rejects connection/); +# per-rule authentication_timeout re-arms the timer +set_hba(<<'EOF'); +{"children": [ + {"transport": "local", "auth": {"method": "trust"}}, + {"address": "127.0.0.1/32", "auth": {"method": "scram-sha-256"}, + "settings": {"authentication_timeout": "7"}} +]} +EOF +$node->reload; +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=alice', + 'per-rule authentication_timeout applied', + sql => 'SHOW authentication_timeout', + expected_stdout => qr/^7s$/m); + +# a list setting as an array, quoted as the parameter needs +set_hba(<<'EOF'); +{"children": [ + {"transport": "local", "auth": {"method": "trust"}}, + {"address": "127.0.0.1/32", "auth": {"method": "trust"}, + "settings": {"search_path": ["My Schema", "public"]}} +]} +EOF +$node->reload; +$node->connect_ok( + 'host=127.0.0.1 dbname=postgres user=alice', + 'array setting joined into a list', + sql => 'SHOW search_path', + expected_stdout => qr/^"My Schema", public$/m); + # rejected shapes, one per rule, visible in the view; queried from a # session opened beforehand, since a backend that loads the rules itself # cannot start on them @@ -346,7 +453,10 @@ set_hba(<<'EOF'); {"users": 42, "auth": {"method": "trust"}}, {"import": "legacy_hba.conf", "children": []}, {"users": "alice"}, - {"databases": "postgres", "children": []}, + {"settings": {"work_mem": "1MB"}, "children": []}, + {"auth": {"method": "trust"}, "settings": {"no_such_guc": "1"}}, + {"auth": {"method": "trust"}, "settings": {"shared_buffers": "1GB"}}, + {"auth": {"method": "trust"}, "settings": {"work_mem": "lots"}}, {"auth": {"method": "scrim"}}, {"users": "@missing.txt", "auth": {"method": "trust"}}, {"users": "@empty.txt", "auth": {"method": "trust"}}, @@ -377,6 +487,12 @@ like($res, qr/import node cannot have "children"/, 'children on import rejected'); is(scalar(() = $res =~ /leaf node has no authentication method/g), 2, 'leaf without method rejected, empty children array is a leaf'); +like($res, qr/unrecognized configuration parameter "no_such_guc"/, + 'unknown GUC rejected'); +like($res, qr/"shared_buffers" cannot be changed without restarting/, + 'postmaster GUC rejected'); +like($res, qr/invalid value for parameter "work_mem"/, + 'bad GUC value rejected'); like($res, qr/invalid authentication method "scrim"/, 'parse_hba_line errors pass through'); like($res, qr/could not open file ".*missing\.txt"/, @@ -389,22 +505,21 @@ like($res, qr/inherited "ssl" cannot be used with transport "local"/, 'inherited constraint on local child rejected'); like($res, qr/address "10\.0\.0\.1" must be in CIDR notation/, 'address without prefix length rejected'); -is(scalar(() = $res =~ /\n/g) + 1, 13, 'one error per bad rule'); +is(scalar(() = $res =~ /\n/g) + 1, 16, 'one error per bad rule'); # a rejected reload keeps the rules loaded before $node->reload; $node->wait_for_log(qr/pg_hba\.json was not reloaded/); SKIP: { - skip 'new backends load the rules themselves with EXEC_BACKEND', 2 + skip 'new backends load the rules themselves with EXEC_BACKEND', 3 if $exec_backend eq 'on'; - $node->connect_ok('host=127.0.0.1 dbname=postgres user=all', - 'old rules still in effect after a rejected reload'); - $node->connect_fails( + $node->connect_ok( 'host=127.0.0.1 dbname=postgres user=alice', - 'old rules still reject what they rejected', - expected_stderr => qr/rejects connection/); + 'old rules still in effect after a rejected reload', + sql => 'SHOW search_path', + expected_stdout => qr/^"My Schema", public$/m); } # problems with the document itself fail the load as a whole, and the diff --git a/src/test/regress/expected/guc.out b/src/test/regress/expected/guc.out index 0c18fc94e31..3b5bd31c0d6 100644 --- a/src/test/regress/expected/guc.out +++ b/src/test/regress/expected/guc.out @@ -1006,3 +1006,12 @@ SELECT name FROM tab_settings_flags (0 rows) DROP TABLE tab_settings_flags; +-- hba-level parameter: settable at startup/reload and from hba, not later +SELECT context FROM pg_settings WHERE name = 'authentication_timeout'; + context +--------- + hba +(1 row) + +SET authentication_timeout = 10; +ERROR: parameter "authentication_timeout" cannot be changed now diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out index 935406548cf..1b1227f4492 100644 --- a/src/test/regress/expected/rules.out +++ b/src/test/regress/expected/rules.out @@ -1372,8 +1372,9 @@ pg_hba_file_rules| SELECT rule_number, auth_method, options, node_path, + settings, error - FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, node_path, error); + FROM pg_hba_file_rules() a(rule_number, file_name, line_number, type, database, user_name, address, netmask, auth_method, options, node_path, settings, error); pg_hosts_file_entries| SELECT entry_number, file_name, line_number, @@ -1383,8 +1384,9 @@ pg_hosts_file_entries| SELECT entry_number, ssl_ca_file, ssl_passphrase_command, ssl_passphrase_command_supports_reload, + settings, error - FROM pg_hosts_file_entries() a(entry_number, file_name, line_number, hostname, ssl_cert_file, ssl_key_file, ssl_ca_file, ssl_passphrase_command, ssl_passphrase_command_supports_reload, error); + FROM pg_hosts_file_entries() a(entry_number, file_name, line_number, hostname, ssl_cert_file, ssl_key_file, ssl_ca_file, ssl_passphrase_command, ssl_passphrase_command_supports_reload, settings, error); pg_ident_file_mappings| SELECT map_number, file_name, line_number, diff --git a/src/test/regress/sql/guc.sql b/src/test/regress/sql/guc.sql index e78b4af3a3a..617c3d9d0d6 100644 --- a/src/test/regress/sql/guc.sql +++ b/src/test/regress/sql/guc.sql @@ -406,3 +406,6 @@ SELECT name FROM tab_settings_flags WHERE no_reset AND NOT no_reset_all ORDER BY 1; DROP TABLE tab_settings_flags; +-- hba-level parameter: settable at startup/reload and from hba, not later +SELECT context FROM pg_settings WHERE name = 'authentication_timeout'; +SET authentication_timeout = 10; diff --git a/src/test/ssl/t/005_json_hosts.pl b/src/test/ssl/t/005_json_hosts.pl index 4aa484e9d00..f6fec468533 100644 --- a/src/test/ssl/t/005_json_hosts.pl +++ b/src/test/ssl/t/005_json_hosts.pl @@ -85,6 +85,7 @@ sub session_query my $datadir = $node->data_dir; my $hosts_json = "$datadir/pg_hosts.json"; +my $hba_json = "$datadir/pg_hba.json"; sub write_file { @@ -131,13 +132,26 @@ ok(unlink("$datadir/pg_hosts.conf"), 'legacy hosts file removed'); # JSON hosts file ############################################################################## +# The hba rules move to a JSON file too, with a rule carrying a setting +# in front of the imported pg_hba.conf, to check that an hba setting +# overrides a hosts setting for the same parameter. +write_file($hba_json, <<"EOF"); +{"hba": {"children": [ + {"address": "$SERVERHOSTCIDR", "ssl": "require", + "databases": "trustdb", "users": "anotheruser", + "auth": {"method": "trust"}, + "settings": {"work_mem": "12MB"}}, + {"import": "pg_hba.conf"}]}} +EOF $node->append_conf('postgresql.conf', <<"EOF"); ssl_sni = on hosts_file = '$hosts_json' +hba_file = '$hba_json' EOF -# example.org gets its CA from the defaults, the default entry overrides -# the certificate, the no-SNI entry takes everything from the defaults. +# example.org gets its CA from the defaults and carries a setting, the +# default entry overrides the certificate and sets a list parameter from +# an array, the no-SNI entry takes everything from the defaults. # example.net and example.com come from an imported legacy file that reads # its host names from an @file. write_file("$datadir/hosts_extra.conf", @@ -150,8 +164,10 @@ my $hosts = <<'EOF'; "ssl_cert_file": "server-cn-only.crt", "ssl_key_file": "server-cn-only.key"}, "entries": { - "example.org": {"ssl_cert_file": "server-cn-only+server_ca.crt"}, - "*": {"ssl_cert_file": "server-cn-only.crt"}, + "example.org": {"ssl_cert_file": "server-cn-only+server_ca.crt", + "settings": {"work_mem": "11MB"}}, + "*": {"ssl_cert_file": "server-cn-only.crt", + "settings": {"search_path": ["My Schema", "public"]}}, "/no_sni/": {} }} EOF @@ -160,10 +176,14 @@ $node->restart; $node->connect_ok( "$connstr host=example.org sslrootcert=ssl/root_ca.crt sslmode=verify-ca", - 'per-host certificate selected from JSON entries'); + 'per-host certificate selected from JSON entries', + sql => q{SELECT setting, source FROM pg_settings WHERE name = 'work_mem'}, + expected_stdout => qr/^11264\|hosts$/m); $node->connect_ok( "$connstr host=example.net sslrootcert=ssl/root_ca.crt sslmode=verify-ca", - 'certificate of an imported legacy entry selected'); + 'certificate of an imported legacy entry selected', + sql => q{SELECT setting, source FROM pg_settings WHERE name = 'work_mem'}, + expected_stdout => qr/^4096\|default$/m); $node->connect_ok( "$connstr host=example.com sslrootcert=ssl/root_ca.crt sslmode=verify-ca", 'certificate of an imported @file host name selected'); @@ -173,26 +193,37 @@ $node->connect_ok( $node->connect_ok( "$connstr host=other.example.org sslrootcert=ssl/root+server_ca.crt " . "sslmode=require", - 'default entry used without matching name'); + 'default entry used without matching name', + sql => q{SELECT setting, source FROM pg_settings WHERE name = 'work_mem'}, + expected_stdout => qr/^4096\|default$/m); $node->connect_fails( "$connstr host=other.example.org sslrootcert=ssl/root_ca.crt " . "sslmode=verify-ca", 'default entry has no intermediate CA', expected_stderr => qr/certificate verify failed/); +# The hba rule for anotheruser sets the same parameter, and wins. +$node->connect_ok( + "$connstr user=anotheruser host=example.org " + . "sslrootcert=ssl/root_ca.crt sslmode=verify-ca", + 'hba setting overrides the hosts setting', + sql => q{SELECT setting, source FROM pg_settings WHERE name = 'work_mem'}, + expected_stdout => qr/^12288\|hba$/m); + $res = view_query( q{ SELECT entry_number, regexp_replace(file_name, '.*/', ''), hostname, regexp_replace(ssl_cert_file, '.*/', ''), ssl_ca_file, - ssl_passphrase_command_supports_reload, error + ssl_passphrase_command_supports_reload, settings::text, error FROM pg_hosts_file_entries ORDER BY entry_number, hostname}); is( $res, - qq{1|hosts_extra.conf|example.com|server-cn-only+server_ca.crt||f|\n} - . qq{1|hosts_extra.conf|example.net|server-cn-only+server_ca.crt||f|\n} + qq{1|hosts_extra.conf|example.com|server-cn-only+server_ca.crt||f||\n} + . qq{1|hosts_extra.conf|example.net|server-cn-only+server_ca.crt||f||\n} . qq{2|pg_hosts.json|example.org|server-cn-only+server_ca.crt|} - . qq{root_ca.crt|f|\n} - . qq{3|pg_hosts.json|*|server-cn-only.crt|root_ca.crt|f|\n} - . qq{4|pg_hosts.json|/no_sni/|server-cn-only.crt|root_ca.crt|f|}, + . qq{root_ca.crt|f|{"work_mem": "11MB"}|\n} + . qq{3|pg_hosts.json|*|server-cn-only.crt|root_ca.crt|f|} + . qq{{"search_path": "\\"My Schema\\", public"}|\n} + . qq{4|pg_hosts.json|/no_sni/|server-cn-only.crt|root_ca.crt|f||}, 'pg_hosts_file_entries lists JSON entries with defaults applied'); ############################################################################## @@ -217,13 +248,16 @@ $node->reload; $node->wait_for_log(qr/duplicate hosts entry "EXAMPLE.org"/, $log_offset); SKIP: { - skip 'new backends load the SSL configuration with EXEC_BACKEND', 2 + skip 'new backends load the SSL configuration with EXEC_BACKEND', 3 if $exec_backend eq 'on'; $node->connect_ok( "$connstr host=example.org sslrootcert=ssl/root_ca.crt " . "sslmode=verify-ca", - 'old hosts configuration kept after failed reload'); + 'old hosts configuration kept after failed reload', + sql => + q{SELECT setting, source FROM pg_settings WHERE name = 'work_mem'}, + expected_stdout => qr/^11264\|hosts$/m); } ($res) = session_query( q{SELECT hostname, error FROM pg_hosts_file_entries @@ -243,8 +277,8 @@ write_file( is($res, 'example.net|duplicate hosts entry "example.net"', 'duplicate of an imported hostname shown in view'); -# Per-entry problems: a non-object entry, an unrecognized key, and a -# wrong type. +# Per-entry problems: a non-object entry, an unrecognized key, a wrong +# type, and a setting that does not validate. write_file( "$datadir/conf.d/dup.json", qq({"hosts": {"entries": {\n) @@ -252,14 +286,18 @@ write_file( . qq( "b.example.org": {"ssl_cert_file": "server-cn-only.crt", ) . qq("ssl_key_file": "server-cn-only.key", "ssl_crl_file": "x"},\n) . qq( "c.example.org": {"ssl_cert_file": "server-cn-only.crt", ) - . qq("ssl_key_file": 1}}}}\n)); + . qq("ssl_key_file": 1},\n) + . qq( "d.example.org": {"ssl_cert_file": "server-cn-only.crt", ) + . qq("ssl_key_file": "server-cn-only.key", ) + . qq("settings": {"work_mem": "lots"}}}}}\n)); ($res) = session_query( q{SELECT hostname, error FROM pg_hosts_file_entries WHERE error IS NOT NULL ORDER BY hostname}); is( $res, qq{a.example.org|hosts entry must be an object, not string\n} . qq{b.example.org|unrecognized key "ssl_crl_file"\n} - . qq{c.example.org|"ssl_key_file" must be a string}, + . qq{c.example.org|"ssl_key_file" must be a string\n} + . qq{d.example.org|invalid value for parameter "work_mem": "lots"}, 'per-entry errors shown in view'); # A section of the wrong shape is a file-level error, as is a key the -- 2.56.0