Hi,

On 10/5/22 9:24 AM, Michael Paquier wrote:
On Tue, Sep 20, 2022 at 01:33:09PM +0200, Drouvot, Bertrand wrote:
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
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.

Please find attached v5 addressing this. I started with an union but it turns out that we still need the plain string when a regex is used. This is not needed for the authentication per say but for fill_hba_line(). So I ended up creating a new struct without union in v5.

The hostname is different, of course, requiring only an extra
field for its type, or something like that.

I'm using the same new struct as described above for the hostname.


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)?


Right, I added more examples in v5.

-$node->append_conf('postgresql.conf', "log_connections = on\n");
+$node->append_conf(
+    'postgresql.conf', qq{
+listen_addresses = '127.0.0.1'
+log_connections = 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.

Good point, I moved the hostname related tests in src/test/ssl.

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..6fe8c40098 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->is_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);
 
 
 /*
@@ -574,24 +578,30 @@ 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)
 {
        ListCell   *cell;
-       AuthToken  *tok;
+       AuthTokenOrRegex *tokreg;
 
        foreach(cell, tokens)
        {
-               tok = lfirst(cell);
-               if (!tok->quoted && tok->string[0] == '+')
+               tokreg = lfirst(cell);
+               if (!token_is_regexp(tokreg))
                {
-                       if (is_member(roleid, tok->string + 1))
+                       if (!tokreg->authtoken->quoted && 
tokreg->authtoken->string[0] == '+')
+                       {
+                               if (is_member(roleid, tokreg->authtoken->string 
+ 1))
+                                       return true;
+                       }
+                       else if (token_matches(tokreg->authtoken, role) ||
+                                        token_is_keyword(tokreg->authtoken, 
"all"))
                                return true;
                }
-               else if (token_matches(tok, role) ||
-                                token_is_keyword(tok, "all"))
+               else if (token_regexec(role, tokreg->regex))
                        return true;
        }
        return false;
@@ -604,36 +614,41 @@ static bool
 check_db(const char *dbname, const char *role, Oid roleid, List *tokens)
 {
        ListCell   *cell;
-       AuthToken  *tok;
+       AuthTokenOrRegex *tokreg;
 
        foreach(cell, tokens)
        {
-               tok = lfirst(cell);
-               if (am_walsender && !am_db_walsender)
-               {
-                       /*
-                        * physical replication walsender connections can only 
match
-                        * replication keyword
-                        */
-                       if (token_is_keyword(tok, "replication"))
-                               return true;
-               }
-               else if (token_is_keyword(tok, "all"))
-                       return true;
-               else if (token_is_keyword(tok, "sameuser"))
+               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;
-               }
-               else if (token_is_keyword(tok, "samegroup") ||
-                                token_is_keyword(tok, "samerole"))
-               {
-                       if (is_member(roleid, dbname))
+                       else if (token_is_keyword(tokreg->authtoken, 
"sameuser"))
+                       {
+                               if (strcmp(dbname, role) == 0)
+                                       return true;
+                       }
+                       else if (token_is_keyword(tokreg->authtoken, 
"samegroup") ||
+                                        token_is_keyword(tokreg->authtoken, 
"samerole"))
+                       {
+                               if (is_member(roleid, dbname))
+                                       return true;
+                       }
+                       else if (token_is_keyword(tokreg->authtoken, 
"replication"))
+                               continue;               /* never match this if 
not walsender */
+                       else if (token_matches(tokreg->authtoken, 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, tokreg->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 AuthTokenOrRegex *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->authtoken->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->authtoken->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,31 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
        tokens = lfirst(field);
        foreach(tokencell, tokens)
        {
-               parsedline->databases = lappend(parsedline->databases,
-                                                                               
copy_auth_token(lfirst(tokencell)));
+               AuthTokenOrRegex *tokreg;
+               AuthToken  *tok = lfirst(tokencell);
+
+               tokreg = (AuthTokenOrRegex *) palloc0(sizeof(AuthTokenOrRegex));
+               tokreg->authtoken = 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;
+
+                       tokreg->is_regex = true;
+                       re = (regex_t *) palloc(sizeof(regex_t));
+                       if (token_regcomp(re, tok->string + 1, HbaFileName, 
line_num,
+                                                         err_msg, elevel))
+                               tokreg->regex = re;
+                       else
+                               return NULL;
+               }
+               else
+                       tokreg->is_regex = false;
+
+               parsedline->databases = lappend(parsedline->databases, tokreg);
        }
 
        /* Get the roles. */
@@ -1072,8 +1115,31 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
        tokens = lfirst(field);
        foreach(tokencell, tokens)
        {
-               parsedline->roles = lappend(parsedline->roles,
-                                                                       
copy_auth_token(lfirst(tokencell)));
+               AuthTokenOrRegex *tokreg;
+               AuthToken  *tok = lfirst(tokencell);
+
+               tokreg = (AuthTokenOrRegex *) palloc0(sizeof(AuthTokenOrRegex));
+               tokreg->authtoken = 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;
+
+                       tokreg->is_regex = true;
+                       re = (regex_t *) palloc(sizeof(regex_t));
+                       if (token_regcomp(re, tok->string + 1, HbaFileName, 
line_num,
+                                                         err_msg, elevel))
+                               tokreg->regex = re;
+                       else
+                               return NULL;
+               }
+               else
+                       tokreg->is_regex = false;
+
+               parsedline->roles = lappend(parsedline->roles, tokreg);
        }
 
        if (parsedline->conntype != ctLocal)
@@ -1120,6 +1186,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 +1195,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 +1220,16 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
                                parsedline->addrlen = gai_result->ai_addrlen;
                        }
                        else if (ret == EAI_NONAME)
-                               parsedline->hostname = str;
+                       {
+                               parsedline->tok_hostname.is_regex = false;
+
+                               /*
+                                * 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.authtoken = 
copy_auth_token(token);
+                       }
                        else
                        {
                                ereport(elevel,
@@ -1168,9 +1248,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.authtoken)
                                {
                                        ereport(elevel,
                                                        
(errcode(ERRCODE_CONFIG_FILE_ERROR),
@@ -1199,7 +1279,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
                                parsedline->masklen = parsedline->addrlen;
                                pfree(str);
                        }
-                       else if (!parsedline->hostname)
+                       else if (!parsedline->tok_hostname.authtoken && 
!is_regexp)
                        {
                                /* Read the mask field. */
                                pfree(str);
@@ -1261,9 +1341,25 @@ 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));
+                               parsedline->tok_hostname.is_regex = true;
+                               if (!token_regcomp(re,
+                                                                  
token->string + 1, HbaFileName,
+                                                                  line_num, 
err_msg, elevel))
+                                       return NULL;
+
+                               parsedline->tok_hostname.regex = re;
+                       }
                }
        }                                                       /* != ctLocal */
-
        /* Get the authentication method */
        field = lnext(tok_line->fields, field);
        if (!field)
@@ -2132,10 +2228,11 @@ check_hba(hbaPort *port)
                        switch (hba->ip_cmp_method)
                        {
                                case ipCmpMask:
-                                       if (hba->hostname)
+                                       if (hba->tok_hostname.authtoken || 
hba->tok_hostname.is_regex)
                                        {
                                                if (!check_hostname(port,
-                                                                               
        hba->hostname))
+                                                                               
        &hba->tok_hostname))
+
                                                        continue;
                                        }
                                        else
@@ -2342,34 +2439,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 +2778,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..2b7ab0bd63 100644
--- a/src/backend/utils/adt/hbafuncs.c
+++ b/src/backend/utils/adt/hbafuncs.c
@@ -242,9 +242,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc 
tupdesc,
 
                        foreach(lc, hba->databases)
                        {
-                               AuthToken  *tok = lfirst(lc);
+                               AuthTokenOrRegex *tok = lfirst(lc);
 
-                               names = lappend(names, tok->string);
+                               names = lappend(names, tok->authtoken->string);
                        }
                        values[index++] = 
PointerGetDatum(strlist_to_textarray(names));
                }
@@ -259,9 +259,9 @@ fill_hba_line(Tuplestorestate *tuple_store, TupleDesc 
tupdesc,
 
                        foreach(lc, hba->roles)
                        {
-                               AuthToken  *tok = lfirst(lc);
+                               AuthTokenOrRegex *tok = lfirst(lc);
 
-                               roles = lappend(roles, tok->string);
+                               roles = lappend(roles, tok->authtoken->string);
                        }
                        values[index++] = 
PointerGetDatum(strlist_to_textarray(roles));
                }
@@ -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.authtoken)
+                                       addrstr = 
hba->tok_hostname.authtoken->string;
                                else
                                {
                                        /*
diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h
index d06da81806..7a53a31771 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -77,6 +77,32 @@ 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.
+ */
+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;
+
 typedef struct HbaLine
 {
        int                     linenumber;
@@ -89,7 +115,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;
+       AuthTokenOrRegex tok_hostname;
        UserAuth        auth_method;
        char       *usermap;
        char       *pamservice;
@@ -132,17 +158,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 93df77aa4e..5063a2601c 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,4 +208,37 @@ 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:/]);
+
 done_testing();
diff --git a/src/test/ssl/t/002_scram.pl b/src/test/ssl/t/002_scram.pl
index deaa4aa086..b575557b37 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