Received: from malur.postgresql.org ([217.196.149.56]) by arkaria.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1ofym5-0000pP-9P for pgsql-hackers@arkaria.postgresql.org; Wed, 05 Oct 2022 07:25:02 +0000 Received: from localhost ([127.0.0.1] helo=malur.postgresql.org) by malur.postgresql.org with esmtp (Exim 4.92) (envelope-from ) id 1ofym2-0002Hq-CX for pgsql-hackers@arkaria.postgresql.org; Wed, 05 Oct 2022 07:24:58 +0000 Received: from magus.postgresql.org ([2a02:c0:301:0:ffff::29]) by malur.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1ofym0-0002HX-OK for pgsql-hackers@lists.postgresql.org; Wed, 05 Oct 2022 07:24:58 +0000 Received: from out5-smtp.messagingengine.com ([66.111.4.29]) by magus.postgresql.org with esmtps (TLS1.3:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.92) (envelope-from ) id 1ofylv-0002Dv-1w for pgsql-hackers@lists.postgresql.org; Wed, 05 Oct 2022 07:24:56 +0000 Received: from compute4.internal (compute4.nyi.internal [10.202.2.44]) by mailout.nyi.internal (Postfix) with ESMTP id 4DBBF5C011B; Wed, 5 Oct 2022 03:24:48 -0400 (EDT) Received: from mailfrontend2 ([10.202.2.163]) by compute4.internal (MEProxy); Wed, 05 Oct 2022 03:24:48 -0400 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=paquier.xyz; h= cc:cc:content-type:date:date:from:from:in-reply-to:in-reply-to :message-id:mime-version:references:reply-to:sender:subject :subject:to:to; s=fm2; t=1664954688; x=1665041088; bh=EKEsgOLn9q ntEuREhtrADWnQo6p5Wk/00QGDYrMJ4X0=; b=PHU3+/BaOeYCOdt9XSMKJnJleq bUT4lMpPI5YJT9flLlNTIRz8nsUFkY2qj5t+wZzPRHdCIApSRWoB5irmXwdHOjTq UG/RnJA/S1kb6c2iDNaXAjpKTCM0jFmatGDbpWiZOl+x04um9GDy4DTEABfU+FEl /UvI9dKAcqJd6HcJEKV9+FWPwBKp02Nl6YgxwiVeymjsybSP3+ArkkXEVY6cZQlF Vb0y70YZJ5tTWR6/TFHAOnj4Gcg1gcf3jdve/hMoO5de07OjkJIzRouBqakRAo0q Uqg8qmzFAF5cmMH/3sIjKS65Pk2l9C61U/262GIWEjje3uPqizw5E91l20OQ== DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d= messagingengine.com; h=cc:cc:content-type:date:date:feedback-id :feedback-id:from:from:in-reply-to:in-reply-to:message-id :mime-version:references:reply-to:sender:subject:subject:to:to :x-me-proxy:x-me-proxy:x-me-sender:x-me-sender:x-sasl-enc; s= fm2; t=1664954688; x=1665041088; bh=EKEsgOLn9qntEuREhtrADWnQo6p5 Wk/00QGDYrMJ4X0=; b=fzdwBKjr28kcE9HUbTr9gYUyecSJeiw4MVdi1Y/QNTtS pFBIYSYPBplSAGpX2C8uut5igPV90OBd47iG2hHhSURCf3eeopnTAh3tCUYTvmOX c3q3qLAk1ANO1ZrIelPnjH/NXkObeCG3aZnzN7wmd5OekkhutP3hh3WzRr4KcCf/ DCTfUox0WhJSJOFbmWvwS0okWQ88mdtPXlPGRKZZQlTkDYP3eXMsiodqwcjAh9Oz nPvwzZWOV6u2lW5Tp5bUa2z+05CW+GShNHRRC8W8Hray914gXdKYnqX25C2QnI0d T5tpF101W4qBTIa3Sf4plsx9K5MIvGblxh41upQv1g== X-ME-Sender: X-ME-Received: X-ME-Proxy-Cause: gggruggvucftvghtrhhoucdtuddrgedvfedrfeeivddguddvtdcutefuodetggdotefrod ftvfcurfhrohhfihhlvgemucfhrghsthforghilhdpqfgfvfdpuffrtefokffrpgfnqfgh necuuegrihhlohhuthemuceftddtnecusecvtfgvtghiphhivghnthhsucdlqddutddtmd enfghrlhcuvffnffculdejtddmnecujfgurhepfffhvfevuffkfhggtggujgesghdtreer tddtvdenucfhrhhomhepofhitghhrggvlhcurfgrqhhuihgvrhcuoehmihgthhgrvghlse hprghquhhivghrrdighiiiqeenucggtffrrghtthgvrhhnpefhjefgveeuiedvhedvfeej gfeljeeikedtffdvvdefgeekffdufeehkedvieeiudenucffohhmrghinheptghonhhfrd hithenucevlhhushhtvghrufhiiigvpedtnecurfgrrhgrmhepmhgrihhlfhhrohhmpehm ihgthhgrvghlsehprghquhhivghrrdighiii X-ME-Proxy: Feedback-ID: i0fe9450f:Fastmail Received: by mail.messagingengine.com (Postfix) with ESMTPA; Wed, 5 Oct 2022 03:24:46 -0400 (EDT) Date: Wed, 5 Oct 2022 16:24:43 +0900 From: Michael Paquier To: "Drouvot, Bertrand" Cc: Tom Lane , Jacob Champion , PostgreSQL Hackers Subject: Re: Patch proposal: make use of regular expressions for the username in pg_hba.conf Message-ID: References: <4177827.1662684400@sss.pgh.pa.us> <572017.1663646973@sss.pgh.pa.us> MIME-Version: 1.0 Content-Type: multipart/signed; micalg=pgp-sha256; protocol="application/pgp-signature"; boundary="8okZdy99qDIorX0I" Content-Disposition: inline In-Reply-To: List-Id: List-Help: List-Subscribe: List-Post: List-Owner: List-Archive: Archived-At: Precedence: bulk --8okZdy99qDIorX0I Content-Type: multipart/mixed; boundary="lrdNuwwvCJu2Sc7T" Content-Disposition: inline --lrdNuwwvCJu2Sc7T Content-Type: text/plain; charset=us-ascii Content-Disposition: inline Content-Transfer-Encoding: quoted-printable On Tue, Sep 20, 2022 at 01:33:09PM +0200, Drouvot, Bertrand wrote: > I assume (maybe i should not) that if objects starting with / already exi= st > there is very good reason(s) behind. Then I don't think that preventing > their creation in the DDL would help (quite the contrary for the ones that > really need them). I have been pondering on this point for the last few weeks, and I'd like to change my opinion and side with Tom on this one as per the very unlikeliness of this being a problem in the wild. I have studied the places that would require restrictions but that was just feeling adding a bit more bloat into the CREATE/ALTER ROLE paths for what's aimed at providing a consistent experience for the user across pg_hba.conf and pg_ident.conf. > It looks to me that adding a GUC (off by default) to enable/disable the > regexp usage in the hba could be a fair compromise. It won't block any > creation starting with a / and won't open more doors (if such objects exi= st) > by default. Enforcing a behavior change in HBA policies with a GUC does not strike me as a good thing in the long term. I am ready to bet that it would just sit around for nothing like the compatibility GUCs. Anyway, I have looked at the patch. + List *roles_re; + List *databases_re; + regex_t hostname_re; I am surprised by the approach of using separate lists for the regular expressions and the raw names. Wouldn't it be better to store everything in a single list but assign an entry type? In this case it=20 would be either regex or plain string. This would minimize the footprint of the changes (no extra arguments *_re in the routines checking for a match on the roles, databases or hosts). And it seems to me that this would make unnecessary the use of re_num here and there. The hostname is different, of course, requiring only an extra field for its type, or something like that. Perhaps the documentation would gain in clarity if there were more examples, like a set of comma-separated examples (mix of regex and raw strings for example, for all the field types that gain support for regexes)? -$node->append_conf('postgresql.conf', "log_connections =3D on\n"); +$node->append_conf( + 'postgresql.conf', qq{ +listen_addresses =3D '127.0.0.1' +log_connections =3D on +}); Hmm. I think that we may need to reconsider the location of the tests for the regexes with the host name, as the "safe" regression tests should not switch listen_addresses. One location where we already do that is src/test/ssl/, so these could be moved there. Keeping the database and user name parts in src/test/authentication/ is fine. Something that stood out on a first review is the refactoring of 001_password.pl that can be done independently of the main patch: - test_role() -> test_conn() to be able to pass down a database name. - reset_pg_hba() to control the host, db and user parts. The host part does not really apply after moving the hosts checks to a more secure location, so I guess that this had better be extended just for the user and database, keeping host=3Dlocal all the time. I am planning to apply 0001 attached independently, reducing the footprint of 0002, which is your previous patch left untouched (mostly!). -- Michael --lrdNuwwvCJu2Sc7T Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="v4-0001-Refactor-TAP-test-001_password.pl.patch" Content-Transfer-Encoding: quoted-printable =46rom 203122ff1eeae942fc78b5d7ad85122547a2a6ed Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Wed, 5 Oct 2022 15:14:41 +0900 Subject: [PATCH v4 1/2] Refactor TAP test 001_password.pl --- src/test/authentication/t/001_password.pl | 60 ++++++++++++----------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentic= ation/t/001_password.pl index 58e4176e80..9e02697355 100644 --- a/src/test/authentication/t/001_password.pl +++ b/src/test/authentication/t/001_password.pl @@ -24,28 +24,30 @@ if (!$use_unix_sockets) sub reset_pg_hba { my $node =3D shift; + my $host =3D shift; + my $database =3D shift; + my $role =3D shift; my $hba_method =3D shift; =20 unlink($node->data_dir . '/pg_hba.conf'); # just for testing purposes, use a continuation line - $node->append_conf('pg_hba.conf', "local all all\\\n $hba_method"); + $node->append_conf('pg_hba.conf', "$host $database $role\\\n $hba_method"= ); $node->reload; return; } =20 -# Test access for a single role, useful to wrap all tests into one. Extra -# named parameters are passed to connect_ok/fails as-is. -sub test_role +# Test access for a connection string, useful to wrap all tests into one. +# Extra named parameters are passed to connect_ok/fails as-is. +sub test_conn { local $Test::Builder::Level =3D $Test::Builder::Level + 1; =20 - my ($node, $role, $method, $expected_res, %params) =3D @_; + my ($node, $connstr, $method, $expected_res, %params) =3D @_; my $status_string =3D 'failed'; $status_string =3D 'success' if ($expected_res eq 0); =20 - my $connstr =3D "user=3D$role"; my $testname =3D - "authentication $status_string for method $method, role $role"; + "authentication $status_string for method $method, connstr $connstr"; =20 if ($expected_res eq 0) { @@ -81,10 +83,10 @@ $ENV{"PGPASSWORD"} =3D 'pass'; =20 # For "trust" method, all users should be able to connect. These users are= not # considered to be authenticated. -reset_pg_hba($node, 'trust'); -test_role($node, 'scram_role', 'trust', 0, +reset_pg_hba($node, 'local','all', 'all', 'trust'); +test_conn($node, 'user=3Dscram_role', 'trust', 0, log_unlike =3D> [qr/connection authenticated:/]); -test_role($node, 'md5_role', 'trust', 0, +test_conn($node, 'user=3Dmd5_role', 'trust', 0, log_unlike =3D> [qr/connection authenticated:/]); =20 # SYSTEM_USER is null when not authenticated. @@ -106,40 +108,40 @@ is($res, 't', ); =20 # For plain "password" method, all users should also be able to connect. -reset_pg_hba($node, 'password'); -test_role($node, 'scram_role', 'password', 0, +reset_pg_hba($node, 'local','all', 'all', 'password'); +test_conn($node, 'user=3Dscram_role', 'password', 0, log_like =3D> [qr/connection authenticated: identity=3D"scram_role" method=3Dpassword= /]); -test_role($node, 'md5_role', 'password', 0, +test_conn($node, 'user=3Dmd5_role', 'password', 0, log_like =3D> [qr/connection authenticated: identity=3D"md5_role" method=3Dpassword/]= ); =20 # For "scram-sha-256" method, user "scram_role" should be able to connect. -reset_pg_hba($node, 'scram-sha-256'); -test_role( +reset_pg_hba($node, 'local','all', 'all', 'scram-sha-256'); +test_conn( $node, - 'scram_role', + 'user=3Dscram_role', 'scram-sha-256', 0, log_like =3D> [ qr/connection authenticated: identity=3D"scram_role" method=3Dscram-sha-= 256/ ]); -test_role($node, 'md5_role', 'scram-sha-256', 2, +test_conn($node, 'user=3Dmd5_role', 'scram-sha-256', 2, log_unlike =3D> [qr/connection authenticated:/]); =20 # Test that bad passwords are rejected. $ENV{"PGPASSWORD"} =3D 'badpass'; -test_role($node, 'scram_role', 'scram-sha-256', 2, +test_conn($node, 'user=3Dscram_role', 'scram-sha-256', 2, log_unlike =3D> [qr/connection authenticated:/]); $ENV{"PGPASSWORD"} =3D 'pass'; =20 # For "md5" method, all users should be able to connect (SCRAM # authentication will be performed for the user with a SCRAM secret.) -reset_pg_hba($node, 'md5'); -test_role($node, 'scram_role', 'md5', 0, +reset_pg_hba($node, 'local','all', 'all', 'md5'); +test_conn($node, 'user=3Dscram_role', 'md5', 0, log_like =3D> [qr/connection authenticated: identity=3D"scram_role" method=3Dmd5/]); -test_role($node, 'md5_role', 'md5', 0, +test_conn($node, 'user=3Dmd5_role', 'md5', 0, log_like =3D> [qr/connection authenticated: identity=3D"md5_role" method=3Dmd5/]); =20 @@ -164,13 +166,13 @@ is($res, 't', =20 # Tests for channel binding without SSL. # Using the password authentication method; channel binding can't work -reset_pg_hba($node, 'password'); +reset_pg_hba($node, 'local','all', 'all', 'password'); $ENV{"PGCHANNELBINDING"} =3D 'require'; -test_role($node, 'scram_role', 'scram-sha-256', 2); +test_conn($node, 'user=3Dscram_role', 'scram-sha-256', 2); # SSL not in use; channel binding still can't work -reset_pg_hba($node, 'scram-sha-256'); +reset_pg_hba($node, 'local','all', 'all', 'scram-sha-256'); $ENV{"PGCHANNELBINDING"} =3D 'require'; -test_role($node, 'scram_role', 'scram-sha-256', 2); +test_conn($node, 'user=3Dscram_role', 'scram-sha-256', 2); =20 # Test .pgpass processing; but use a temp file, don't overwrite the real o= ne! my $pgpassfile =3D "${PostgreSQL::Test::Utils::tmp_check}/pgpass"; @@ -187,15 +189,15 @@ append_to_file( !); chmod 0600, $pgpassfile or die; =20 -reset_pg_hba($node, 'password'); -test_role($node, 'scram_role', 'password from pgpass', 0); -test_role($node, 'md5_role', 'password from pgpass', 2); +reset_pg_hba($node, 'local','all', 'all', 'password'); +test_conn($node, 'user=3Dscram_role', 'password from pgpass', 0); +test_conn($node, 'user=3Dmd5_role', 'password from pgpass', 2); =20 append_to_file( $pgpassfile, qq! *:*:*:md5_role:p\\ass !); =20 -test_role($node, 'md5_role', 'password from pgpass', 0); +test_conn($node, 'user=3Dmd5_role', 'password from pgpass', 0); =20 done_testing(); --=20 2.37.3 --lrdNuwwvCJu2Sc7T Content-Type: text/plain; charset=us-ascii Content-Disposition: attachment; filename="v4-0002-Main-patch-from-Bertrand-Drouvot.patch" Content-Transfer-Encoding: quoted-printable =46rom cd8debbc5c54f5342cd7db4e43091532fbdb065d Mon Sep 17 00:00:00 2001 =46rom: Michael Paquier Date: Wed, 5 Oct 2022 16:17:28 +0900 Subject: [PATCH v4 2/2] Main patch from Bertrand Drouvot --- src/include/libpq/hba.h | 3 + src/backend/libpq/hba.c | 225 ++++++++++++++++++---- src/test/authentication/t/001_password.pl | 42 ++++ doc/src/sgml/client-auth.sgml | 26 ++- 4 files changed, 245 insertions(+), 51 deletions(-) diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index d06da81806..24fe502d06 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -120,6 +120,9 @@ typedef struct HbaLine char *radiusidentifiers_s; List *radiusports; char *radiusports_s; + List *roles_re; + List *databases_re; + regex_t hostname_re; } HbaLine; =20 typedef struct IdentLine diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index 4637426d62..89a4fa8b98 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -117,6 +117,9 @@ static List *tokenize_inc_file(List *tokens, const char= *outer_filename, const char *inc_filename, int elevel, char **err_msg); static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, int elevel, char **err_msg); +static bool token_regcomp(regex_t *re, char *string, char *filename, + int line_num, char **err_msg, int elevel); +static bool token_regexec(const char *match, regex_t *re); =20 =20 /* @@ -574,13 +577,15 @@ is_member(Oid userid, const char *role) } =20 /* - * Check AuthToken list for a match to role, allowing group names. + * Check AuthToken list for a match to role. + * We are allowing group names and regular expressions. */ static bool -check_role(const char *role, Oid roleid, List *tokens) +check_role(const char *role, Oid roleid, List *tokens, List *tokens_re) { ListCell *cell; AuthToken *tok; + int re_num =3D 0; =20 foreach(cell, tokens) { @@ -590,6 +595,23 @@ check_role(const char *role, Oid roleid, List *tokens) if (is_member(roleid, tok->string + 1)) return true; } + else if (!tok->quoted && tok->string[0] =3D=3D '/') + { + /* + * When tok->string starts with a slash, treat it as a regular + * expression. + */ + ListCell *cell_re; + regex_t *re; + + cell_re =3D list_nth_cell(tokens_re, re_num); + re =3D lfirst(cell_re); + + if (token_regexec(role, re)) + return true; + + re_num++; + } else if (token_matches(tok, role) || token_is_keyword(tok, "all")) return true; @@ -601,10 +623,11 @@ check_role(const char *role, Oid roleid, List *tokens) * Check to see if db/role combination matches AuthToken list. */ static bool -check_db(const char *dbname, const char *role, Oid roleid, List *tokens) +check_db(const char *dbname, const char *role, Oid roleid, List *tokens, L= ist *tokens_re) { ListCell *cell; AuthToken *tok; + int re_num =3D 0; =20 foreach(cell, tokens) { @@ -633,6 +656,23 @@ check_db(const char *dbname, const char *role, Oid rol= eid, List *tokens) } else if (token_is_keyword(tok, "replication")) continue; /* never match this if not walsender */ + else if (!tok->quoted && tok->string[0] =3D=3D '/') + { + /* + * When tok->string starts with a slash, treat it as a regular + * expression. + */ + ListCell *cell_re; + regex_t *re; + + cell_re =3D list_nth_cell(tokens_re, re_num); + re =3D lfirst(cell_re); + + if (token_regexec(dbname, re)) + return true; + + re_num++; + } else if (token_matches(tok, dbname)) return true; } @@ -681,7 +721,7 @@ hostname_match(const char *pattern, const char *actual_= hostname) * Check to see if a connecting IP matches a given host name. */ static bool -check_hostname(hbaPort *port, const char *hostname) +check_hostname(hbaPort *port, const char *hostname, regex_t re) { struct addrinfo *gai_result, *gai; @@ -712,8 +752,17 @@ check_hostname(hbaPort *port, const char *hostname) port->remote_hostname =3D pstrdup(remote_hostname); } =20 + if (hostname[0] =3D=3D '/') + { + /* + * When hostname starts with a slash, treat it as a regular + * expression. + */ + if (!token_regexec(port->remote_hostname, &re)) + return false; + } /* Now see if remote host name matches this pg_hba line */ - if (!hostname_match(hostname, port->remote_hostname)) + else if (!hostname_match(hostname, port->remote_hostname)) return false; =20 /* If we already verified the forward lookup, we're done */ @@ -939,13 +988,13 @@ parse_hba_line(TokenizedAuthLine *tok_line, int eleve= l) struct addrinfo *gai_result; struct addrinfo hints; int ret; - char *cidr_slash; char *unsupauth; ListCell *field; List *tokens; ListCell *tokencell; AuthToken *token; HbaLine *parsedline; + char *cidr_slash =3D NULL; /* keep compiler quiet */ =20 parsedline =3D palloc0(sizeof(HbaLine)); parsedline->linenumber =3D line_num; @@ -1049,9 +1098,27 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elev= el) return NULL; } parsedline->databases =3D NIL; + parsedline->databases_re =3D NIL; tokens =3D lfirst(field); foreach(tokencell, tokens) { + AuthToken *tok =3D lfirst(tokencell); + + if (!tok->quoted && tok->string[0] =3D=3D '/') + { + /* + * When tok->string starts with a slash, treat it as a regular + * expression. Pre-compile it. + */ + regex_t *re; + + re =3D (regex_t *) palloc(sizeof(regex_t)); + if (token_regcomp(re, tok->string + 1, HbaFileName, line_num, + err_msg, elevel)) + parsedline->databases_re =3D lappend(parsedline->databases_re, re); + else + return NULL; + } parsedline->databases =3D lappend(parsedline->databases, copy_auth_token(lfirst(tokencell))); } @@ -1069,9 +1136,27 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elev= el) return NULL; } parsedline->roles =3D NIL; + parsedline->roles_re =3D NIL; tokens =3D lfirst(field); foreach(tokencell, tokens) { + AuthToken *tok =3D lfirst(tokencell); + + if (!tok->quoted && tok->string[0] =3D=3D '/') + { + /* + * When tok->string starts with a slash, treat it as a regular + * expression. Pre-compile it. + */ + regex_t *re; + + re =3D (regex_t *) palloc(sizeof(regex_t)); + if (token_regcomp(re, tok->string + 1, HbaFileName, line_num, + err_msg, elevel)) + parsedline->roles_re =3D lappend(parsedline->roles_re, re); + else + return NULL; + } parsedline->roles =3D lappend(parsedline->roles, copy_auth_token(lfirst(tokencell))); } @@ -1120,6 +1205,8 @@ parse_hba_line(TokenizedAuthLine *tok_line, int eleve= l) } else { + bool is_regexp =3D token->string[0] =3D=3D '/' ? true : false; + /* IP and netmask are specified */ parsedline->ip_cmp_method =3D ipCmpMask; =20 @@ -1127,9 +1214,12 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elev= el) str =3D pstrdup(token->string); =20 /* Check if it has a CIDR suffix and if so isolate it */ - cidr_slash =3D strchr(str, '/'); - if (cidr_slash) - *cidr_slash =3D '\0'; + if (!is_regexp) + { + cidr_slash =3D strchr(str, '/'); + if (cidr_slash) + *cidr_slash =3D '\0'; + } =20 /* Get the IP address either way */ hints.ai_flags =3D AI_NUMERICHOST; @@ -1168,7 +1258,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int eleve= l) pg_freeaddrinfo_all(hints.ai_family, gai_result); =20 /* Get the netmask */ - if (cidr_slash) + if (cidr_slash && !is_regexp) { if (parsedline->hostname) { @@ -1199,7 +1289,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int eleve= l) parsedline->masklen =3D parsedline->addrlen; pfree(str); } - else if (!parsedline->hostname) + else if (!parsedline->hostname && !is_regexp) { /* Read the mask field. */ pfree(str); @@ -1261,6 +1351,18 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elev= el) return NULL; } } + else if (is_regexp) + { + /* + * When token->string starts with a slash, treat it as a + * regular expression. Pre-compile it. + */ + if (!token_regcomp(&parsedline->hostname_re, + token->string + 1, HbaFileName, + line_num, err_msg, elevel)) + return NULL; + parsedline->hostname =3D str; + } } } /* !=3D ctLocal */ =20 @@ -2135,7 +2237,8 @@ check_hba(hbaPort *port) if (hba->hostname) { if (!check_hostname(port, - hba->hostname)) + hba->hostname, hba->hostname_re)) + continue; } else @@ -2162,10 +2265,10 @@ check_hba(hbaPort *port) =20 /* Check database and role */ if (!check_db(port->database_name, port->user_name, roleid, - hba->databases)) + hba->databases, hba->databases_re)) continue; =20 - if (!check_role(port->user_name, roleid, hba->roles)) + if (!check_role(port->user_name, roleid, hba->roles, hba->roles_re)) continue; =20 /* Found a record that matched! */ @@ -2342,34 +2445,9 @@ parse_ident_line(TokenizedAuthLine *tok_line, int el= evel) * When system username starts with a slash, treat it as a regular * expression. Pre-compile it. */ - int r; - pg_wchar *wstr; - int wlen; - - wstr =3D palloc((strlen(parsedline->ident_user + 1) + 1) * sizeof(pg_wch= ar)); - wlen =3D pg_mb2wchar_with_len(parsedline->ident_user + 1, - wstr, strlen(parsedline->ident_user + 1)); - - r =3D pg_regcomp(&parsedline->re, wstr, wlen, REG_ADVANCED, C_COLLATION_= OID); - if (r) - { - char errstr[100]; - - pg_regerror(r, &parsedline->re, errstr, sizeof(errstr)); - ereport(elevel, - (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION), - errmsg("invalid regular expression \"%s\": %s", - parsedline->ident_user + 1, errstr), - errcontext("line %d of configuration file \"%s\"", - line_num, IdentFileName))); - - *err_msg =3D psprintf("invalid regular expression \"%s\": %s", - parsedline->ident_user + 1, errstr); - - pfree(wstr); + if (!token_regcomp(&parsedline->re, parsedline->ident_user + 1, + IdentFileName, line_num, err_msg, elevel)) return NULL; - } - pfree(wstr); } =20 return parsedline; @@ -2706,3 +2784,68 @@ hba_authname(UserAuth auth_method) =20 return UserAuthName[auth_method]; } + +/* + * Compile the regular expression "re" and return whether it compiles + * successfully or not. + * + * If not, the last 4 parameters are used to add extra details while repor= ting + * the error. + */ +static bool +token_regcomp(regex_t *re, char *string, char *filename, int line_num, + char **err_msg, int elevel) +{ + int r; + pg_wchar *wstr; + int wlen; + + wstr =3D palloc((strlen(string) + 1) * sizeof(pg_wchar)); + wlen =3D pg_mb2wchar_with_len(string, + wstr, strlen(string)); + + r =3D pg_regcomp(re, wstr, wlen, REG_ADVANCED, C_COLLATION_OID); + if (r) + { + char errstr[100]; + + pg_regerror(r, re, errstr, sizeof(errstr)); + ereport(elevel, + (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION), + errmsg("invalid regular expression \"%s\": %s", + string, errstr), + errcontext("line %d of configuration file \"%s\"", + line_num, filename))); + + *err_msg =3D psprintf("invalid regular expression \"%s\": %s", + string, errstr); + + pfree(wstr); + return false; + } + + pfree(wstr); + return true; +} + +/* + * Return whether "match" is matching the regular expression "re" or not. + */ +static bool +token_regexec(const char *match, regex_t *re) +{ + pg_wchar *wmatchstr; + int wmatchlen; + + wmatchstr =3D palloc((strlen(match) + 1) * sizeof(pg_wchar)); + wmatchlen =3D pg_mb2wchar_with_len(match, wmatchstr, strlen(match)); + + if (pg_regexec(re, wmatchstr, wmatchlen, 0, NULL, 0, NULL, 0) =3D=3D REG_= OKAY) + { + pfree(wmatchstr); + return true; + } + + pfree(wmatchstr); + return false; +} diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentic= ation/t/001_password.pl index 9e02697355..453dafffdb 100644 --- a/src/test/authentication/t/001_password.pl +++ b/src/test/authentication/t/001_password.pl @@ -81,6 +81,9 @@ $node->safe_psql( GRANT ALL ON sysuser_data TO md5_role;"); $ENV{"PGPASSWORD"} =3D 'pass'; =20 +# Create a database to test regular expression +$node->safe_psql('postgres', "CREATE database regex_testdb;"); + # For "trust" method, all users should be able to connect. These users are= not # considered to be authenticated. reset_pg_hba($node, 'local','all', 'all', 'trust'); @@ -200,4 +203,43 @@ append_to_file( =20 test_conn($node, 'user=3Dmd5_role', 'password from pgpass', 0); =20 +# Testing with regular expression for username. Note that the first regex +# matches in this case. +reset_pg_hba($node, 'local', 'all', '/^.*nomatch.*$, baduser, /^md.*$', + 'password'); +test_conn($node, 'user=3Dmd5_role', 'password, matching regexp for usernam= e', 0); +# The third regex does not match anymore. +reset_pg_hba($node, 'local', 'all', '/^.*nomatch.*$, baduser, /^m_d.*$', + 'password'); +test_conn($node, 'user=3Dmd5_role', 'password, non matching regexp for use= rname', 2, + log_unlike =3D> [qr/connection authenticated:/]); + +# Testing with regular expression for dbname. The third regex matches. +reset_pg_hba($node, 'local', '/^.*nomatch.*$, baddb, /^regex_t.*b$', 'all'= , 'password'); +test_conn($node, 'user=3Dmd5_role dbname=3Dregex_testdb', 'password, + matching regexp for dbname', 0); +# The third regex does not match anymore. +reset_pg_hba($node, 'local', '/^.*nomatch.*$, baddb, /^regex_t.*ba$', 'all= ', 'password'); +test_conn($node, 'user=3Dmd5_role dbname=3Dregex_testdb', 'password, non m= atching regexp for dbname', 2, + log_unlike =3D> [qr/connection authenticated:/]); + +# Testing with regular expression for hostname +SKIP: +{ + # Being able to do a reverse lookup of a hostname on Windows for localhost + # is not guaranteed on all environments by default. + # So, skip the regular expression test for hostname on Windows. + skip "Regular expression for hostname not tested on Windows", 2 if ($wind= ows_os); + + # XXX: this makes the test unsecure. + $node->append_conf('postgresql.conf', "listen_addresses =3D '127.0.0.1'\n= "); + $node->restart; + + reset_pg_hba($node, 'host', 'all', 'all /^.*$', 'password'); + test_conn($node, 'user=3Dmd5_role host=3Dlocalhost', 'password, matching = regexp for hostname', 0); + + reset_pg_hba($node, 'host', 'all', 'all /^$', 'password'); + test_conn($node, 'user=3Dmd5_role host=3Dlocalhost', 'password, non match= ing regexp for hostname', 2); +} + done_testing(); diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index c6f1b70fd3..28343445be 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -235,8 +235,9 @@ hostnogssenc database userPostgreSQL database. - Multiple database names can be supplied by separating them with - commas. A separate file containing database names can be specified= by + Multiple database names and/or regular expressions preceded by / + can be supplied by separating them with commas. + A separate file containing database names can be specified by preceding the file name with @. @@ -249,7 +250,8 @@ hostnogssenc database userall specifies that it matches all users. Otherwise, this is either the name of a specific - database user, or a group name preceded by +. + database user, a regular expression preceded by / + or a group name preceded by +. (Recall that there is no real distinction between users and groups in PostgreSQL; a + ma= rk really means match any of the roles that are directly or indirectly membe= rs @@ -258,7 +260,8 @@ hostnogssenc database user/ + can be supplied by separating them with commas. A separate file containing user names can be specified by preceding= the file name with @. @@ -270,8 +273,9 @@ hostnogssenc database user Specifies the client machine address(es) that this record - matches. This field can contain either a host name, an IP - address range, or one of the special key words mentioned below. + matches. This field can contain either a host name, a regular expr= ession + preceded by / representing host names, an IP add= ress range, + or one of the special key words mentioned below. =20 @@ -785,16 +789,18 @@ host all all 192.168.12.10= /32 gss # TYPE DATABASE USER ADDRESS METHOD host all all 192.168.0.0/16 ident map= =3Domicron =20 -# If these are the only three lines for local connections, they will +# If these are the only four lines for local connections, they will # allow local users to connect only to their own databases (databases -# with the same name as their database user name) except for administrators -# and members of role "support", who can connect to all databases. The fi= le -# $PGDATA/admins contains a list of names of administrators. Passwords +# with the same name as their database user name) except for administrator= s, +# users finishing with "helpdesk" and members of role "support", +# who can connect to all databases. +# The file$PGDATA/admins contains a list of names of administrators. Pass= words # are required in all cases. # # TYPE DATABASE USER ADDRESS METHOD local sameuser all md5 local all @admins md5 +local all /^.*helpdesk$ md5 local all +support md5 =20 # The last two lines above can be combined into a single line: --=20 2.37.3 --lrdNuwwvCJu2Sc7T-- --8okZdy99qDIorX0I Content-Type: application/pgp-signature; name="signature.asc" -----BEGIN PGP SIGNATURE----- iQIzBAEBCAAdFiEEG72nH6vTowiyblFKnvQgOdbyQH0FAmM9MTsACgkQnvQgOdby QH05kQ/+JEKWlCrluHZNcns1w0Q8m9gGfU1z/cFpzlWhXHEpMgY018Jgs9XDcrDj EvzAmYun3urwKnZnvbjQ1ERq+N/b8yOVgE3uifzchqFXcDxZpfmDcPCgH21WX3/v Owtkjj0vRPO0Wp/EAOytH9whwqMUSVYuYq07h5gknkwnZEGPOU9ipjJk0s9NjRoG /mK91tsFMROZ/7TAeqHs2m10KCq1rBkUVDmODng0lmyY0Csgu2PPZ1IsEidFvP8V FYW/KAFPFyu5OZLbI4jwwpWB4pL7mUfOza+Dds2PA6nAQei+gD6qTJOlDSDoLe41 YgoHweL1yk2uXABRoi7R8ejl8l9ImxJOEkJ/xsseRsKfcOAuXNKaBcRMt1ruO3kZ baCWfL9nYh6tEy7HsLcVFviH0BqGV1rExXMcnd08bK6eqAe099pnqvsYTSaK7Jl+ tfa5uX0H3mx3A5jY6Ih6jYlVsq0rxkiU4BF1Hk6H5TlYYMXb7XLI3yB6+emUNzEe 8Y2fQAywtUJnvWheoB9NMZyl577D+VjVFKDV3WWuTCgrg7Ecg9UFMn3SMOBkeYNR utYiFbaqWwMh1qnLYWMetWeyPqleUgH6LfFaZ9+VWO7vxmbjzJJHYthwdF7FsDtH FOL9p5C0aJE5w9xqfN2XPPOLe9NFYTL363V3XxfdDPcS7+K45r8= =zJxX -----END PGP SIGNATURE----- --8okZdy99qDIorX0I--