diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index c6f1b70fd3..30753003ba 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -234,10 +234,13 @@ hostnogssenc database userPostgreSQL database.
- Multiple database names can be supplied by separating them with
- commas. A separate file containing database names can be specified by
- preceding the file name with @.
+ a specific PostgreSQL database or a regular
+ expression preceded by /.
+ Multiple database names and/or regular expressions preceded by /
+ can be supplied by separating them with commas.
+ A separate file containing database names and/or regular expressions preceded
+ by / can be specified by preceding the file name
+ with @.
@@ -249,18 +252,20 @@ hostnogssenc database userall specifies that it
matches all users. Otherwise, this is either the name of a specific
- database user, or a group name preceded by +.
+ database user, a regular expression preceded by /,
+ or a group name preceded by +.
(Recall that there is no real distinction between users and groups
in PostgreSQL; a + mark really means
match any of the roles that are directly or indirectly members
of this role
, while a name without a + mark matches
- only that specific role.) For this purpose, a superuser is only
+ only that specific role or regular expression.) For this purpose, a superuser is only
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.
- A separate file containing user names can be specified by preceding the
- file name with @.
+ Multiple user names and/or regular expressions preceded by /
+ can be supplied by separating them with commas. A separate file containing
+ user names and/or regular expressions preceded by /
+ can be specified by preceding the file name with @.
@@ -739,6 +744,18 @@ host all all ::1/128 trust
# TYPE DATABASE USER ADDRESS METHOD
host all all localhost 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 localhost 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 localhost 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,15 +802,17 @@ 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 users ending
+# with "helpdesk", administrators 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 /^.*helpdesk$ md5
local all @admins md5
local all +support md5
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c
index 947a1edcef..165ef4d397 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -119,7 +119,8 @@ 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 int regcomp_auth_token(AuthToken *token);
+static int regcomp_auth_token(AuthToken *token, char *filename, int line_num,
+ char **err_msg, int elevel);
static int regexec_auth_token(const char *match, AuthToken *token,
size_t nmatch, regmatch_t pmatch[]);
@@ -308,7 +309,8 @@ copy_auth_token(AuthToken *in)
* input. Returns the result of pg_regcomp().
*/
static int
-regcomp_auth_token(AuthToken *token)
+regcomp_auth_token(AuthToken *token, char *filename, int line_num,
+ char **err_msg, int elevel)
{
pg_wchar *wstr;
int wlen;
@@ -325,6 +327,22 @@ regcomp_auth_token(AuthToken *token)
wstr, strlen(token->string + 1));
rc = pg_regcomp(token->regex, wstr, wlen, REG_ADVANCED, C_COLLATION_OID);
+ if (rc)
+ {
+ char errstr[100];
+
+ pg_regerror(rc, token->regex, errstr, sizeof(errstr));
+ ereport(elevel,
+ (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
+ errmsg("invalid regular expression \"%s\": %s",
+ token->string + 1, errstr),
+ errcontext("line %d of configuration file \"%s\"",
+ line_num, filename)));
+
+ *err_msg = psprintf("invalid regular expression \"%s\": %s",
+ token->string + 1, errstr);
+
+ }
pfree(wstr);
return rc;
@@ -652,13 +670,18 @@ check_role(const char *role, Oid roleid, List *tokens)
foreach(cell, tokens)
{
tok = lfirst(cell);
- if (!tok->quoted && tok->string[0] == '+')
+ if (!token_has_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 (regexec_auth_token(role, tok, 0, NULL) == REG_OKAY)
return true;
}
return false;
@@ -685,22 +708,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_has_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 (regexec_auth_token(dbname, tok, 0, NULL) == REG_OKAY)
return true;
}
return false;
@@ -1119,8 +1147,15 @@ 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 = copy_auth_token(lfirst(tokencell));
+
+ /*
+ * Compile a regex from the database token, if necessary.
+ */
+ if (regcomp_auth_token(tok, HbaFileName, line_num, err_msg, elevel))
+ return NULL;
+
+ parsedline->databases = lappend(parsedline->databases, tok);
}
/* Get the roles. */
@@ -1139,8 +1174,15 @@ 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 = copy_auth_token(lfirst(tokencell));
+
+ /*
+ * Compile a regex from the role token, if necessary.
+ */
+ if (regcomp_auth_token(tok, HbaFileName, line_num, err_msg, elevel))
+ return NULL;
+
+ parsedline->roles = lappend(parsedline->roles, tok);
}
if (parsedline->conntype != ctLocal)
@@ -2374,7 +2416,6 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
List *tokens;
AuthToken *token;
IdentLine *parsedline;
- int rc;
Assert(tok_line->fields != NIL);
field = list_head(tok_line->fields);
@@ -2410,24 +2451,8 @@ parse_ident_line(TokenizedAuthLine *tok_line, int elevel)
* Now that the field validation is done, compile a regex from the user
* token, if necessary.
*/
- rc = regcomp_auth_token(parsedline->token);
- if (rc)
- {
- char errstr[100];
-
- pg_regerror(rc, parsedline->token->regex, errstr, sizeof(errstr));
- ereport(elevel,
- (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
- errmsg("invalid regular expression \"%s\": %s",
- parsedline->token->string + 1, errstr),
- errcontext("line %d of configuration file \"%s\"",
- line_num, IdentFileName)));
-
- *err_msg = psprintf("invalid regular expression \"%s\": %s",
- parsedline->token->string + 1, errstr);
-
+ if (regcomp_auth_token(parsedline->token, IdentFileName, line_num, err_msg, elevel))
return NULL;
- }
return parsedline;
}
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index ea664d18f5..acb8bfaac8 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"};