pg.ddx.io  pgsql-hackers@postgresql.org mailing list archive  
help / color / mirror / Atom feed
From: Drouvot, Bertrand <bertranddrouvot.pg@gmail.com>
To: Michael Paquier <michael@paquier.xyz>
Cc: Tom Lane <tgl@sss.pgh.pa.us>
Cc: Jacob Champion <jchampion@timescale.com>
Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Subject: Re: Patch proposal: make use of regular expressions for the username in pg_hba.conf
Date: Wed, 12 Oct 2022 08:17:14 +0200
Message-ID: <607af3c9-e45d-3941-41a2-d21065d6594b@gmail.com> (raw)
In-Reply-To: <Y0UNP4FJyOBn6WX5@paquier.xyz>
References: <fff0d7c1-8ad4-76a1-9db3-0ab6ec338bf7@amazon.com>
	<CAAWbhmhifLJfCGk1zMjWNHg+uGo9SohdSEqsFo0wa2rBNCB85g@mail.gmail.com>
	<4177827.1662684400@sss.pgh.pa.us>
	<CAAWbhmjxPcvUAKhpb1mu8PvszaiL7yFi_0ZUQVbfiowsFVaW4A@mail.gmail.com>
	<Yyk5rfYUTylD8XRg@paquier.xyz>
	<572017.1663646973@sss.pgh.pa.us>
	<YylB+vmF9dti1Alq@paquier.xyz>
	<e3091081-dc98-7f2b-829f-c247b9337653@gmail.com>
	<Yz0xO0emJ+mxtj2a@paquier.xyz>
	<e346f82f-4a6b-22f7-d6c1-cef16d472de9@gmail.com>
	<Y0UNP4FJyOBn6WX5@paquier.xyz>

Hi,

On 10/11/22 8:29 AM, Michael Paquier wrote:
> On Mon, Oct 10, 2022 at 09:00:06AM +0200, Drouvot, Bertrand wrote:
>>   	foreach(cell, tokens)
>>   	{
>> [...]
>> +		tokreg = lfirst(cell);
>> +		if (!token_is_regexp(tokreg))
>>   		{
>> -			if (strcmp(dbname, role) == 0)
>> +			if (am_walsender && !am_db_walsender)
>> +			{
>> +				/*
>> +				 * physical replication walsender connections can only match
>> +				 * replication keyword
>> +				 */
>> +				if (token_is_keyword(tokreg->authtoken, "replication"))
>> +					return true;
>> +			}
>> +			else if (token_is_keyword(tokreg->authtoken, "all"))
>>   				return true;
> 
> When checking the list of databases in check_db(), physical WAL
> senders (aka am_walsender && !am_db_walsender) would be able to accept
> regexps, but these should only accept "replication" and never a
> regexp, no?  

Oh right, good catch, thanks! Please find attached v6 fixing it.


> This is kind of special in the HBA logic, coming back to 9.0 where
> physical replication and this special role property have been
> introduced.  WAL senders have gained an actual database property later
> on in 9.4 with logical decoding, keeping "replication" for
> compatibility (connection strings can use replication=database to
> connect as a non-physical WAL sender and connect to a specific
> database).
> 

Thanks for the explanation!

>> +typedef struct AuthToken
>> +{
>> +	char	   *string;
>> +	bool		quoted;
>> +} AuthToken;
>> +
>> +/*
>> + * Distinguish the case a token has to be treated as a regular
>> + * expression or not.
>> + */
>> +typedef struct AuthTokenOrRegex
>> +{
>> +	bool		is_regex;
>> +
>> +	/*
>> +	 * Not an union as we still need the token string for fill_hba_line().
>> +	 */
>> +	AuthToken  *authtoken;
>> +	regex_t    *regex;
>> +} AuthTokenOrRegex;
> 
> Hmm.  With is_regex to check if a regex_t exists, both structures may
> not be necessary. 

Agree that both struct are not necessary. In v6, AuthTokenOrRegex has 
been removed and the regex has been moved to AuthToken. There is no 
is_regex bool anymore, as it's enough to test whether regex is NULL or not.

> I have not put my hands on that directly, but if
> I guess that I would shape things to have only AuthToken with
> (enforcing regex_t in priority if set in the list of elements to check
> for a match):
> - the string
> - quoted
> - regex_t
> A list member should never have (regex_t != NULL && quoted), right?

The patch does allow that. For example it happens for the test where we 
add a comma in the role name. As we don't rely on a dedicated char to 
mark the end of a reg exp (we only rely on / to mark its start) then 
allowing (regex_t != NULL && quoted) seems reasonable to me.

>> +# test with a comma in the regular expression
>> +reset_pg_hba($node, 'all', '"/^.*5,.*e$"', 'password');
>> +test_conn($node, 'user=md5,role', 'password', 'matching regexp for username',
>> +	0);
> 
> So, we check here that the role includes "5," in its name.  This is
> getting fun to parse ;)
>

Indeed, ;-)


>>   elsif ($ENV{PG_TEST_EXTRA} !~ /\bssl\b/)
>>   {
>> -	plan skip_all => 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
>> +	plan skip_all =>
>> +	  'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA';
>>   }
> 
> Unrelated noise from perltidy.

Right.

Regards,

-- 
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

Attachments:

v6-0001-hba_with_regexp.patch (text/plain, 23.5K) download diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index c6f1b70fd3..406628ef35 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -235,8 +235,9 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl logical replication connections do specify it. Otherwise, this is the name of a specific <productname>PostgreSQL</productname> 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 <literal>/</literal> + can be supplied by separating them with commas. + A separate file containing database names can be specified by preceding the file name with <literal>@</literal>. </para> </listitem> @@ -249,7 +250,8 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl Specifies which database user name(s) this record matches. The value <literal>all</literal> specifies that it matches all users. Otherwise, this is either the name of a specific - database user, or a group name preceded by <literal>+</literal>. + database user, a regular expression preceded by <literal>/</literal> + or a group name preceded by <literal>+</literal>. (Recall that there is no real distinction between users and groups in <productname>PostgreSQL</productname>; a <literal>+</literal> mark really means <quote>match any of the roles that are directly or indirectly members @@ -258,7 +260,8 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl considered to be a member of a role if they are explicitly a member of the role, directly or indirectly, and not just by virtue of being a superuser. - Multiple user names can be supplied by separating them with commas. + Multiple user names and/or regular expressions preceded by <literal>/</literal> + can be supplied by separating them with commas. A separate file containing user names can be specified by preceding the file name with <literal>@</literal>. </para> @@ -270,8 +273,9 @@ hostnogssenc <replaceable>database</replaceable> <replaceable>user</replaceabl <listitem> <para> 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 expression + preceded by <literal>/</literal> representing host names, an IP address range, + or one of the special key words mentioned below. </para> <para> @@ -739,6 +743,24 @@ host all all ::1/128 trust # TYPE DATABASE USER ADDRESS METHOD host all all localhost trust +# The same using a regular expression for host name, which allows connection for +# host name ending with "test". +# +# TYPE DATABASE USER ADDRESS METHOD +host all all /^.*test$ trust + +# The same using regular expression for DATABASE, which allows connection to the +# db1 and testdb databases and any database with a name ending with "test". +# +# TYPE DATABASE USER ADDRESS METHOD +local db1,/^.*test$,testdb all /^.*test$ trust + +# The same using regular expression for USER, which allows connection to the +# user1 and testuser users and any user with a name ending with "test". +# +# TYPE DATABASE USER ADDRESS METHOD +local db1,/^.*test$,testdb user1,/^.*test$,testuser /^.*test$ trust + # Allow any user from any host with IP address 192.168.93.x to connect # to database "postgres" as the same user name that ident reports for # the connection (typically the operating system user name). @@ -785,16 +807,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=omicron -# 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 file -# $PGDATA/admins contains a list of names of administrators. Passwords +# with the same name as their database user name) except for administrators, +# users ending with "helpdesk" and members of role "support", +# who can connect to all databases. +# The file$PGDATA/admins contains a list of names of administrators. Passwords # 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 # The last two lines above can be combined into a single line: diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index 4637426d62..fc1fb12bb3 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -66,6 +66,7 @@ typedef struct check_network_data } check_network_data; +#define token_is_regexp(t) (t->regex) #define token_is_keyword(t, k) (!t->quoted && strcmp(t->string, k) == 0) #define token_matches(t, k) (strcmp(t->string, k) == 0) @@ -117,6 +118,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); /* @@ -267,7 +271,7 @@ make_auth_token(const char *token, bool quoted) toklen = strlen(token); /* we copy string into same palloc block as the struct */ - authtoken = (AuthToken *) palloc(sizeof(AuthToken) + toklen + 1); + authtoken = (AuthToken *) palloc0(sizeof(AuthToken) + toklen + 1); authtoken->string = (char *) authtoken + sizeof(AuthToken); authtoken->quoted = quoted; memcpy(authtoken->string, token, toklen + 1); @@ -574,7 +578,8 @@ is_member(Oid userid, const char *role) } /* - * 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) @@ -585,13 +590,18 @@ check_role(const char *role, Oid roleid, List *tokens) foreach(cell, tokens) { tok = lfirst(cell); - if (!tok->quoted && tok->string[0] == '+') + if (!token_is_regexp(tok)) { - if (is_member(roleid, tok->string + 1)) + if (!tok->quoted && tok->string[0] == '+') + { + if (is_member(roleid, tok->string + 1)) + return true; + } + else if (token_matches(tok, role) || + token_is_keyword(tok, "all")) return true; } - else if (token_matches(tok, role) || - token_is_keyword(tok, "all")) + else if (token_regexec(role, tok->regex)) return true; } return false; @@ -618,22 +628,27 @@ check_db(const char *dbname, const char *role, Oid roleid, List *tokens) if (token_is_keyword(tok, "replication")) return true; } - else if (token_is_keyword(tok, "all")) - return true; - else if (token_is_keyword(tok, "sameuser")) + else if (token_is_keyword(tok, "replication")) + continue; /* never match this if not walsender */ + else if (!token_is_regexp(tok)) { - if (strcmp(dbname, role) == 0) + if (token_is_keyword(tok, "all")) return true; - } - else if (token_is_keyword(tok, "samegroup") || - token_is_keyword(tok, "samerole")) - { - if (is_member(roleid, dbname)) + else if (token_is_keyword(tok, "sameuser")) + { + if (strcmp(dbname, role) == 0) + return true; + } + else if (token_is_keyword(tok, "samegroup") || + token_is_keyword(tok, "samerole")) + { + if (is_member(roleid, dbname)) + return true; + } + else if (token_matches(tok, dbname)) return true; } - else if (token_is_keyword(tok, "replication")) - continue; /* never match this if not walsender */ - else if (token_matches(tok, dbname)) + else if (token_regexec(dbname, tok->regex)) return true; } return false; @@ -681,7 +696,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 AuthToken *tok_hostname) { struct addrinfo *gai_result, *gai; @@ -712,8 +727,13 @@ check_hostname(hbaPort *port, const char *hostname) port->remote_hostname = pstrdup(remote_hostname); } + if (token_is_regexp(tok_hostname)) + { + if (!token_regexec(port->remote_hostname, tok_hostname->regex)) + return false; + } /* Now see if remote host name matches this pg_hba line */ - if (!hostname_match(hostname, port->remote_hostname)) + else if (!hostname_match(tok_hostname->string, port->remote_hostname)) return false; /* If we already verified the forward lookup, we're done */ @@ -761,7 +781,7 @@ check_hostname(hbaPort *port, const char *hostname) if (!found) elog(DEBUG2, "pg_hba.conf host name \"%s\" rejected because address resolution did not return a match with IP address of client", - hostname); + tok_hostname->string); port->remote_hostname_resolv = found ? +1 : -1; @@ -939,13 +959,13 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) 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 = NULL; /* keep compiler quiet */ parsedline = palloc0(sizeof(HbaLine)); parsedline->linenumber = line_num; @@ -1052,8 +1072,26 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) tokens = lfirst(field); foreach(tokencell, tokens) { - parsedline->databases = lappend(parsedline->databases, - copy_auth_token(lfirst(tokencell))); + AuthToken *tok = lfirst(tokencell); + + tok = copy_auth_token(lfirst(tokencell)); + if (tok->string[0] == '/') + { + /* + * When tok->string starts with a slash, treat it as a regular + * expression. Pre-compile it. + */ + regex_t *re; + + re = (regex_t *) palloc(sizeof(regex_t)); + if (token_regcomp(re, tok->string + 1, HbaFileName, line_num, + err_msg, elevel)) + tok->regex = re; + else + return NULL; + } + + parsedline->databases = lappend(parsedline->databases, tok); } /* Get the roles. */ @@ -1072,8 +1110,26 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) tokens = lfirst(field); foreach(tokencell, tokens) { - parsedline->roles = lappend(parsedline->roles, - copy_auth_token(lfirst(tokencell))); + AuthToken *tok = lfirst(tokencell); + + tok = copy_auth_token(lfirst(tokencell)); + if (tok->string[0] == '/') + { + /* + * When tok->string starts with a slash, treat it as a regular + * expression. Pre-compile it. + */ + regex_t *re; + + re = (regex_t *) palloc(sizeof(regex_t)); + if (token_regcomp(re, tok->string + 1, HbaFileName, line_num, + err_msg, elevel)) + tok->regex = re; + else + return NULL; + } + + parsedline->roles = lappend(parsedline->roles, tok); } if (parsedline->conntype != ctLocal) @@ -1120,6 +1176,8 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) } else { + bool is_regexp = token->string[0] == '/' ? true : false; + /* IP and netmask are specified */ parsedline->ip_cmp_method = ipCmpMask; @@ -1127,9 +1185,12 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) str = pstrdup(token->string); /* Check if it has a CIDR suffix and if so isolate it */ - cidr_slash = strchr(str, '/'); - if (cidr_slash) - *cidr_slash = '\0'; + if (!is_regexp) + { + cidr_slash = strchr(str, '/'); + if (cidr_slash) + *cidr_slash = '\0'; + } /* Get the IP address either way */ hints.ai_flags = AI_NUMERICHOST; @@ -1149,7 +1210,14 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) parsedline->addrlen = gai_result->ai_addrlen; } else if (ret == EAI_NONAME) - parsedline->hostname = str; + { + /* + * This is ok to copy the token->string and not str here, as + * we'll error and report "specifying both host name and CIDR + * mask is invalid" below should they differ. + */ + parsedline->tok_hostname = *copy_auth_token(token); + } else { ereport(elevel, @@ -1168,9 +1236,9 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) pg_freeaddrinfo_all(hints.ai_family, gai_result); /* Get the netmask */ - if (cidr_slash) + if (cidr_slash && !is_regexp) { - if (parsedline->hostname) + if (parsedline->tok_hostname.string) { ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), @@ -1199,7 +1267,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) parsedline->masklen = parsedline->addrlen; pfree(str); } - else if (!parsedline->hostname) + else if (!parsedline->tok_hostname.string && !is_regexp) { /* Read the mask field. */ pfree(str); @@ -1261,6 +1329,22 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel) return NULL; } } + else if (is_regexp) + { + /* + * When token->string starts with a slash, treat it as a + * regular expression. Pre-compile it. + */ + regex_t *re; + + re = (regex_t *) palloc(sizeof(regex_t)); + if (!token_regcomp(re, + token->string + 1, HbaFileName, + line_num, err_msg, elevel)) + return NULL; + + parsedline->tok_hostname.regex = re; + } } } /* != ctLocal */ @@ -2132,10 +2216,11 @@ check_hba(hbaPort *port) switch (hba->ip_cmp_method) { case ipCmpMask: - if (hba->hostname) + if (hba->tok_hostname.string || hba->tok_hostname.regex) { if (!check_hostname(port, - hba->hostname)) + &hba->tok_hostname)) + continue; } else @@ -2342,34 +2427,9 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel) * When system username starts with a slash, treat it as a regular * expression. Pre-compile it. */ - int r; - pg_wchar *wstr; - int wlen; - - wstr = palloc((strlen(parsedline->ident_user + 1) + 1) * sizeof(pg_wchar)); - wlen = pg_mb2wchar_with_len(parsedline->ident_user + 1, - wstr, strlen(parsedline->ident_user + 1)); - - r = 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 = 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); } return parsedline; @@ -2706,3 +2766,68 @@ hba_authname(UserAuth auth_method) 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 reporting + * 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 = palloc((strlen(string) + 1) * sizeof(pg_wchar)); + wlen = pg_mb2wchar_with_len(string, + wstr, strlen(string)); + + r = 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 = 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 = palloc((strlen(match) + 1) * sizeof(pg_wchar)); + wmatchlen = pg_mb2wchar_with_len(match, wmatchstr, strlen(match)); + + if (pg_regexec(re, wmatchstr, wmatchlen, 0, NULL, 0, NULL, 0) == REG_OKAY) + { + pfree(wmatchstr); + return true; + } + + pfree(wmatchstr); + return false; +} diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c index 9e5794071c..fd995e91c8 100644 --- a/src/backend/utils/adt/hbafuncs.c +++ b/src/backend/utils/adt/hbafuncs.c @@ -274,10 +274,8 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, switch (hba->ip_cmp_method) { case ipCmpMask: - if (hba->hostname) - { - addrstr = hba->hostname; - } + if (hba->tok_hostname.string) + addrstr = hba->tok_hostname.string; else { /* diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index d06da81806..595d3c5a46 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -77,6 +77,19 @@ typedef enum ClientCertName clientCertDN } ClientCertName; +/* + * A single string token lexed from an authentication configuration file + * (pg_ident.conf or pg_hba.conf), together with whether the token has + * been quoted. It may also contain a non NULL regular expression if the string + * starts with a "/". + */ +typedef struct AuthToken +{ + char *string; + bool quoted; + regex_t *regex; +} AuthToken; + typedef struct HbaLine { int linenumber; @@ -89,7 +102,7 @@ typedef struct HbaLine struct sockaddr_storage mask; int masklen; /* zero if we don't have a valid mask */ IPCompareMethod ip_cmp_method; - char *hostname; + AuthToken tok_hostname; UserAuth auth_method; char *usermap; char *pamservice; @@ -132,17 +145,6 @@ typedef struct IdentLine regex_t re; } IdentLine; -/* - * A single string token lexed from an authentication configuration file - * (pg_ident.conf or pg_hba.conf), together with whether the token has - * been quoted. - */ -typedef struct AuthToken -{ - char *string; - bool quoted; -} AuthToken; - /* * TokenizedAuthLine represents one line lexed from an authentication * configuration file. Each item in the "fields" list is a sub-list of diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl index ea664d18f5..1c559dfbda 100644 --- a/src/test/authentication/t/001_password.pl +++ b/src/test/authentication/t/001_password.pl @@ -81,6 +81,14 @@ $node->safe_psql( GRANT ALL ON sysuser_data TO md5_role;"); $ENV{"PGPASSWORD"} = 'pass'; +# Create a role that contains a comma to stress the parsing. +$node->safe_psql('postgres', + q{SET password_encryption='md5'; CREATE ROLE "md5,role" LOGIN PASSWORD 'pass';} +); + +# 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, 'all', 'all', 'trust'); @@ -200,6 +208,39 @@ append_to_file( test_conn($node, 'user=md5_role', 'password from pgpass', 0); +# Testing with regular expression for username. Note that the third regex +# matches in this case. +reset_pg_hba($node, 'all', '/^.*nomatch.*$, baduser, /^md.*$', 'password'); +test_conn($node, 'user=md5_role', 'password, matching regexp for username', + 0); + +# The third regex does not match anymore. +reset_pg_hba($node, 'all', '/^.*nomatch.*$, baduser, /^m_d.*$', 'password'); +test_conn($node, 'user=md5_role', + 'password, non matching regexp for username', + 2, log_unlike => [qr/connection authenticated:/]); + +# test with a comma in the regular expression +reset_pg_hba($node, 'all', '"/^.*5,.*e$"', 'password'); +test_conn($node, 'user=md5,role', 'password', 'matching regexp for username', + 0); + +# Testing with regular expression for dbname. The third regex matches. +reset_pg_hba($node, '/^.*nomatch.*$, baddb, /^regex_t.*b$', 'all', + 'password'); +test_conn( + $node, 'user=md5_role dbname=regex_testdb', 'password, + matching regexp for dbname', 0); + +# The third regex does not match anymore. +reset_pg_hba($node, '/^.*nomatch.*$, baddb, /^regex_t.*ba$', + 'all', 'password'); +test_conn( + $node, + 'user=md5_role dbname=regex_testdb', + 'password, non matching regexp for dbname', + 2, log_unlike => [qr/connection authenticated:/]); + unlink($pgpassfile); delete $ENV{"PGPASSFILE"}; diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl index deaa4aa086..471eb3e256 100644 --- a/src/test/ssl/t/002_scram.pl +++ b/src/test/ssl/t/002_scram.pl @@ -22,7 +22,8 @@ if ($ENV{with_ssl} ne 'openssl') } elsif ($ENV{PG_TEST_EXTRA} !~ /\bssl\b/) { - plan skip_all => 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA'; + plan skip_all => + 'Potentially unsafe test SSL not enabled in PG_TEST_EXTRA'; } my $ssl_server = SSL::Server->new(); @@ -37,6 +38,20 @@ sub switch_server_cert $ssl_server->switch_server_cert(@_); } +# Delete pg_hba.conf from the given node, add a new entry to it +# and then execute a reload to refresh it. +sub reset_pg_hba +{ + my $node = shift; + my $hostname = shift; + + unlink($node->data_dir . '/pg_hba.conf'); + # just for testing purposes, use a continuation line + $node->append_conf('pg_hba.conf', "host all all $hostname scram-sha-256"); + $node->reload; + return; +} + # This is the hostname used to connect to the server. my $SERVERHOSTADDR = '127.0.0.1'; @@ -136,4 +151,25 @@ $node->connect_ok( qr/connection authenticated: identity="ssltestuser" method=scram-sha-256/ ]); +# 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 ($windows_os); + + # Test regular expression on hostname, this one matches any host. + reset_pg_hba($node, '/^.*$'); + $node->connect_ok("$common_connstr user=ssltestuser", + "Basic SCRAM authentication with SSL matching regexp on hostname"); + # Test regular expression on hostname, this one does not match. + reset_pg_hba($node, '/^$'); + $node->connect_fails( + "$common_connstr user=ssltestuser", + "Basic SCRAM authentication with SSL non matching regexp on hostname", + log_like => [qr/no pg_hba.conf entry for host/]); +} + done_testing();

view thread (37+ messages)  latest in thread

Message-ID: <607af3c9-e45d-3941-41a2-d21065d6594b@gmail.com>
Permalink:  ../607af3c9-e45d-3941-41a2-d21065d6594b@gmail.com/
Also on:    postgresql.org/message-id/607af3c9-e45d-3941-41a2-d21065d6594b@gmail.com

 ·  · 

reply

Reply instructions:

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

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

  To: pgsql-hackers@postgresql.org
  Cc: bertranddrouvot.pg@gmail.com, michael@paquier.xyz, tgl@sss.pgh.pa.us, jchampion@timescale.com, pgsql-hackers@lists.postgresql.org
  Subject: Re: Patch proposal: make use of regular expressions for the username in pg_hba.conf
  In-Reply-To: <607af3c9-e45d-3941-41a2-d21065d6594b@gmail.com>

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

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