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
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();

Reply via email to