diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml
index c6f1b70fd3..28343445be 100644
--- a/doc/src/sgml/client-auth.sgml
+++ b/doc/src/sgml/client-auth.sgml
@@ -235,8 +235,9 @@ hostnogssenc database userPostgreSQL database.
- Multiple database names can be supplied by separating them with
- commas. A separate file containing database names can be specified by
+ Multiple database names and/or regular expressions preceded by /
+ can be supplied by separating them with commas.
+ A separate file containing database names can be specified by
preceding the file name with @.
@@ -249,7 +250,8 @@ hostnogssenc database userall specifies that it
matches all users. Otherwise, this is either the name of a specific
- database user, or a group name preceded by +.
+ database user, a regular expression preceded by /
+ or a group name preceded by +.
(Recall that there is no real distinction between users and groups
in PostgreSQL; a + mark really means
match any of the roles that are directly or indirectly members
@@ -258,7 +260,8 @@ hostnogssenc database user/
+ can be supplied by separating them with commas.
A separate file containing user names can be specified by preceding the
file name with @.
@@ -270,8 +273,9 @@ hostnogssenc database user
Specifies the client machine address(es) that this record
- matches. This field can contain either a host name, an IP
- address range, or one of the special key words mentioned below.
+ matches. This field can contain either a host name, a regular expression
+ preceded by / representing host names, an IP address range,
+ or one of the special key words mentioned below.
@@ -785,16 +789,18 @@ host all all 192.168.12.10/32 gss
# TYPE DATABASE USER ADDRESS METHOD
host all all 192.168.0.0/16 ident map=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 finishing with "helpdesk" and members of role "support",
+# who can connect to all databases.
+# The file$PGDATA/admins contains a list of names of administrators. 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..200de8d7d8 100644
--- a/src/backend/libpq/hba.c
+++ b/src/backend/libpq/hba.c
@@ -117,6 +117,9 @@ static List *tokenize_inc_file(List *tokens, const char *outer_filename,
const char *inc_filename, int elevel, char **err_msg);
static bool parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
int elevel, char **err_msg);
+static bool token_regcomp(regex_t *re, char *string, char *filename,
+ int line_num, char **err_msg, int elevel);
+static bool token_regexec(const char *match, regex_t *re);
/*
@@ -574,13 +577,15 @@ 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)
+check_role(const char *role, Oid roleid, List *tokens, List *tokens_re)
{
ListCell *cell;
AuthToken *tok;
+ int re_num = 0;
foreach(cell, tokens)
{
@@ -590,6 +595,23 @@ check_role(const char *role, Oid roleid, List *tokens)
if (is_member(roleid, tok->string + 1))
return true;
}
+ else if (!tok->quoted && tok->string[0] == '/')
+ {
+ /*
+ * When tok->string starts with a slash, treat it as a regular
+ * expression.
+ */
+ ListCell *cell_re;
+ regex_t *re;
+
+ cell_re = list_nth_cell(tokens_re, re_num);
+ re = lfirst(cell_re);
+
+ if (token_regexec(role, re))
+ return true;
+
+ re_num++;
+ }
else if (token_matches(tok, role) ||
token_is_keyword(tok, "all"))
return true;
@@ -601,10 +623,11 @@ check_role(const char *role, Oid roleid, List *tokens)
* Check to see if db/role combination matches AuthToken list.
*/
static bool
-check_db(const char *dbname, const char *role, Oid roleid, List *tokens)
+check_db(const char *dbname, const char *role, Oid roleid, List *tokens, List *tokens_re)
{
ListCell *cell;
AuthToken *tok;
+ int re_num = 0;
foreach(cell, tokens)
{
@@ -633,6 +656,23 @@ check_db(const char *dbname, const char *role, Oid roleid, List *tokens)
}
else if (token_is_keyword(tok, "replication"))
continue; /* never match this if not walsender */
+ else if (!tok->quoted && tok->string[0] == '/')
+ {
+ /*
+ * When tok->string starts with a slash, treat it as a regular
+ * expression.
+ */
+ ListCell *cell_re;
+ regex_t *re;
+
+ cell_re = list_nth_cell(tokens_re, re_num);
+ re = lfirst(cell_re);
+
+ if (token_regexec(dbname, re))
+ return true;
+
+ re_num++;
+ }
else if (token_matches(tok, dbname))
return true;
}
@@ -681,7 +721,7 @@ hostname_match(const char *pattern, const char *actual_hostname)
* Check to see if a connecting IP matches a given host name.
*/
static bool
-check_hostname(hbaPort *port, const char *hostname)
+check_hostname(hbaPort *port, const char *hostname, regex_t re)
{
struct addrinfo *gai_result,
*gai;
@@ -712,8 +752,17 @@ check_hostname(hbaPort *port, const char *hostname)
port->remote_hostname = pstrdup(remote_hostname);
}
+ if (hostname[0] == '/')
+ {
+ /*
+ * When hostname starts with a slash, treat it as a regular
+ * expression.
+ */
+ if (!token_regexec(port->remote_hostname, &re))
+ return false;
+ }
/* Now see if remote host name matches this pg_hba line */
- if (!hostname_match(hostname, port->remote_hostname))
+ else if (!hostname_match(hostname, port->remote_hostname))
return false;
/* If we already verified the forward lookup, we're done */
@@ -939,13 +988,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;
@@ -1049,9 +1098,27 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
return NULL;
}
parsedline->databases = NIL;
+ parsedline->databases_re = NIL;
tokens = lfirst(field);
foreach(tokencell, tokens)
{
+ AuthToken *tok = lfirst(tokencell);
+
+ if (!tok->quoted && 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))
+ parsedline->databases_re = lappend(parsedline->databases_re, re);
+ else
+ return NULL;
+ }
parsedline->databases = lappend(parsedline->databases,
copy_auth_token(lfirst(tokencell)));
}
@@ -1069,9 +1136,27 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
return NULL;
}
parsedline->roles = NIL;
+ parsedline->roles_re = NIL;
tokens = lfirst(field);
foreach(tokencell, tokens)
{
+ AuthToken *tok = lfirst(tokencell);
+
+ if (!tok->quoted && 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))
+ parsedline->roles_re = lappend(parsedline->roles_re, re);
+ else
+ return NULL;
+ }
parsedline->roles = lappend(parsedline->roles,
copy_auth_token(lfirst(tokencell)));
}
@@ -1120,6 +1205,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 +1214,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;
@@ -1168,7 +1258,7 @@ 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)
{
@@ -1199,7 +1289,7 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
parsedline->masklen = parsedline->addrlen;
pfree(str);
}
- else if (!parsedline->hostname)
+ else if (!parsedline->hostname && !is_regexp)
{
/* Read the mask field. */
pfree(str);
@@ -1261,6 +1351,18 @@ parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
return NULL;
}
}
+ else if (is_regexp)
+ {
+ /*
+ * When token->string starts with a slash, treat it as a
+ * regular expression. Pre-compile it.
+ */
+ if (!token_regcomp(&parsedline->hostname_re,
+ token->string + 1, HbaFileName,
+ line_num, err_msg, elevel))
+ return NULL;
+ parsedline->hostname = str;
+ }
}
} /* != ctLocal */
@@ -2135,7 +2237,8 @@ check_hba(hbaPort *port)
if (hba->hostname)
{
if (!check_hostname(port,
- hba->hostname))
+ hba->hostname, hba->hostname_re))
+
continue;
}
else
@@ -2162,10 +2265,10 @@ check_hba(hbaPort *port)
/* Check database and role */
if (!check_db(port->database_name, port->user_name, roleid,
- hba->databases))
+ hba->databases, hba->databases_re))
continue;
- if (!check_role(port->user_name, roleid, hba->roles))
+ if (!check_role(port->user_name, roleid, hba->roles, hba->roles_re))
continue;
/* Found a record that matched! */
@@ -2342,34 +2445,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 +2784,58 @@ hba_authname(UserAuth auth_method)
return UserAuthName[auth_method];
}
+
+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;
+}
+
+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/include/libpq/hba.h b/src/include/libpq/hba.h
index 90036f7bcd..c24f9166cb 100644
--- a/src/include/libpq/hba.h
+++ b/src/include/libpq/hba.h
@@ -120,6 +120,9 @@ typedef struct HbaLine
char *radiusidentifiers_s;
List *radiusports;
char *radiusports_s;
+ List *roles_re;
+ List *databases_re;
+ regex_t hostname_re;
} HbaLine;
typedef struct IdentLine
diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl
index 3e3079c824..049ad1cc89 100644
--- a/src/test/authentication/t/001_password.pl
+++ b/src/test/authentication/t/001_password.pl
@@ -23,29 +23,32 @@ if (!$use_unix_sockets)
# and then execute a reload to refresh it.
sub reset_pg_hba
{
- my $node = shift;
- my $hba_method = shift;
+ my $node = shift;
+ my $host = shift;
+ my $database = shift;
+ my $role = shift;
+ my $method = shift;
unlink($node->data_dir . '/pg_hba.conf');
# just for testing purposes, use a continuation line
- $node->append_conf('pg_hba.conf', "local all all\\\n $hba_method");
+ $node->append_conf('pg_hba.conf', "$host $database $role\\\n $method");
$node->reload;
return;
}
# Test access for a single role, useful to wrap all tests into one. Extra
# named parameters are passed to connect_ok/fails as-is.
-sub test_role
+sub test_conn
{
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my ($node, $role, $method, $expected_res, %params) = @_;
+ my ($node, $conn, $method, $expected_res, %params) = @_;
my $status_string = 'failed';
$status_string = 'success' if ($expected_res eq 0);
- my $connstr = "user=$role";
+ my $connstr = "$conn";
my $testname =
- "authentication $status_string for method $method, role $role";
+ "authentication $status_string for method $method, conn $conn";
if ($expected_res eq 0)
{
@@ -61,7 +64,11 @@ sub test_role
# Initialize primary node
my $node = PostgreSQL::Test::Cluster->new('primary');
$node->init;
-$node->append_conf('postgresql.conf', "log_connections = on\n");
+$node->append_conf(
+ 'postgresql.conf', qq{
+listen_addresses = '127.0.0.1'
+log_connections = on
+});
$node->start;
# Create 3 roles with different password methods for each one. The same
@@ -74,61 +81,66 @@ $node->safe_psql('postgres',
);
$ENV{"PGPASSWORD"} = 'pass';
+# Create a database to test regular expression
+$node->safe_psql('postgres',
+ "CREATE database testdb;"
+);
+
# For "trust" method, all users should be able to connect. These users are not
# considered to be authenticated.
-reset_pg_hba($node, 'trust');
-test_role($node, 'scram_role', 'trust', 0,
+reset_pg_hba($node, 'local','all', 'all', 'trust');
+test_conn($node, 'user=scram_role', 'trust', 0,
log_unlike => [qr/connection authenticated:/]);
-test_role($node, 'md5_role', 'trust', 0,
+test_conn($node, 'user=md5_role', 'trust', 0,
log_unlike => [qr/connection authenticated:/]);
# For plain "password" method, all users should also be able to connect.
-reset_pg_hba($node, 'password');
-test_role($node, 'scram_role', 'password', 0,
+reset_pg_hba($node, 'local', 'all', 'all', 'password');
+test_conn($node, 'user=scram_role', 'password', 0,
log_like =>
[qr/connection authenticated: identity="scram_role" method=password/]);
-test_role($node, 'md5_role', 'password', 0,
+test_conn($node, 'user=md5_role', 'password', 0,
log_like =>
[qr/connection authenticated: identity="md5_role" method=password/]);
# For "scram-sha-256" method, user "scram_role" should be able to connect.
-reset_pg_hba($node, 'scram-sha-256');
-test_role(
+reset_pg_hba($node, 'local', 'all', 'all', 'scram-sha-256');
+test_conn(
$node,
- 'scram_role',
+ 'user=scram_role',
'scram-sha-256',
0,
log_like => [
qr/connection authenticated: identity="scram_role" method=scram-sha-256/
]);
-test_role($node, 'md5_role', 'scram-sha-256', 2,
+test_conn($node, 'user=md5_role', 'scram-sha-256', 2,
log_unlike => [qr/connection authenticated:/]);
# Test that bad passwords are rejected.
$ENV{"PGPASSWORD"} = 'badpass';
-test_role($node, 'scram_role', 'scram-sha-256', 2,
+test_conn($node, 'user=scram_role', 'scram-sha-256', 2,
log_unlike => [qr/connection authenticated:/]);
$ENV{"PGPASSWORD"} = 'pass';
# For "md5" method, all users should be able to connect (SCRAM
# authentication will be performed for the user with a SCRAM secret.)
-reset_pg_hba($node, 'md5');
-test_role($node, 'scram_role', 'md5', 0,
+reset_pg_hba($node, 'local', 'all', 'all', 'md5');
+test_conn($node, 'user=scram_role', 'md5', 0,
log_like =>
[qr/connection authenticated: identity="scram_role" method=md5/]);
-test_role($node, 'md5_role', 'md5', 0,
+test_conn($node, 'user=md5_role', 'md5', 0,
log_like =>
[qr/connection authenticated: identity="md5_role" method=md5/]);
# Tests for channel binding without SSL.
# Using the password authentication method; channel binding can't work
-reset_pg_hba($node, 'password');
+reset_pg_hba($node, 'local', 'all', 'all', 'password');
$ENV{"PGCHANNELBINDING"} = 'require';
-test_role($node, 'scram_role', 'scram-sha-256', 2);
+test_conn($node, 'user=scram_role', 'scram-sha-256', 2);
# SSL not in use; channel binding still can't work
-reset_pg_hba($node, 'scram-sha-256');
+reset_pg_hba($node, 'local', 'all', 'all', 'scram-sha-256');
$ENV{"PGCHANNELBINDING"} = 'require';
-test_role($node, 'scram_role', 'scram-sha-256', 2);
+test_conn($node, 'user=scram_role', 'scram-sha-256', 2);
# Test .pgpass processing; but use a temp file, don't overwrite the real one!
my $pgpassfile = "${PostgreSQL::Test::Utils::tmp_check}/pgpass";
@@ -145,15 +157,38 @@ append_to_file(
!);
chmod 0600, $pgpassfile or die;
-reset_pg_hba($node, 'password');
-test_role($node, 'scram_role', 'password from pgpass', 0);
-test_role($node, 'md5_role', 'password from pgpass', 2);
+reset_pg_hba($node, 'local', 'all', 'all', 'password');
+test_conn($node, 'user=scram_role', 'password from pgpass', 0);
+test_conn($node, 'user=md5_role', 'password from pgpass', 2);
append_to_file(
$pgpassfile, qq!
*:*:*:md5_role:p\\ass
!);
-test_role($node, 'md5_role', 'password from pgpass', 0);
+test_conn($node, 'user=md5_role', 'password from pgpass', 0);
+
+# Testing with regular expression for username
+reset_pg_hba($node, 'local', 'all', '/^.*nomatch.*$, baduser, /^.*md.*$', 'password');
+test_conn($node, 'user=md5_role', 'password, matching regexp for username', 0);
+
+reset_pg_hba($node, 'local', 'all', '/^.*nomatch.*$, baduser, /^.*m_d.*$', 'password');
+test_conn($node, 'user=md5_role', 'password, non matching regexp for username', 2,
+ log_unlike => [qr/connection authenticated:/]);
+
+# Testing with regular expression for dbname
+reset_pg_hba($node, 'local', '/^.*nomatch.*$, baddb, /^t.*b$', 'all', 'password');
+test_conn($node, 'user=md5_role dbname=testdb', 'password, matching regexp for dbname', 0);
+
+reset_pg_hba($node, 'local', '/^.*nomatch.*$, baddb, /^t.*ba$', 'all', 'password');
+test_conn($node, 'user=md5_role dbname=testdb', 'password, non matching regexp for dbname', 2,
+ log_unlike => [qr/connection authenticated:/]);
+
+# Testing with regular expression for hostname
+reset_pg_hba($node, 'host', 'all', 'all /^.*$', 'password');
+test_conn($node, 'user=md5_role host=localhost', 'password, matching regexp for hostname', 0);
+
+reset_pg_hba($node, 'host', 'all', 'all /^$', 'password');
+test_conn($node, 'user=md5_role host=localhost', 'password, non matching regexp for hostname', 2);
done_testing();