diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index c6f1b70fd3..32d5d45863 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -233,11 +233,20 @@ 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 @. + Otherwise, this is the name of a specific + PostgreSQL database or a regular expression. + Multiple database names and/or regular expressions can be supplied by + separating them with commas. + + + If the database name starts with a slash (/), the + remainder of the name is treated as a regular expression. + (See for details of + PostgreSQL's regular expression syntax.) + + + A separate file containing database names and/or regular expressions + can be specified by preceding the file name with @. @@ -249,7 +258,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 (when starting with a slash + (/), 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,9 +268,18 @@ hostnogssenc database user@. + Multiple user names and/or regular expressions can be supplied by + separating them with commas. + + + If the user name starts with a slash (/), the + remainder of the name is treated as a regular expression. + (See for details of + PostgreSQL's regular expression syntax.) + + + A separate file containing user names and/or regular expressions can + be specified by preceding the file name with @. @@ -739,6 +758,14 @@ host all all ::1/128 trust # TYPE DATABASE USER ADDRESS METHOD host all all localhost trust +# The same using a regular expression for DATABASE, that allows connection +# to the database db1, db2 and any databases with a name beginning by "db" +# and finishing with a number using two to four digits (like "db1234" or +# "db12"). +# +# TYPE DATABASE USER ADDRESS METHOD +local db1,"/^db\d{2,4}$",db2 all 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 +812,16 @@ 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 -# are required in all cases. +# with the same name as their database user name) except for users whose +# name end 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 f3539a7929..ea92f02a47 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -293,6 +293,30 @@ free_auth_token(AuthToken *token) pg_regfree(token->regex); } +/* + * Free a HbaLine. Its list of AuthTokens for databases and roles may include + * regular expressions that need to be cleaned up explicitly. + */ +static void +free_hba_line(HbaLine *line) +{ + ListCell *cell; + + foreach(cell, line->roles) + { + AuthToken *tok = lfirst(cell); + + free_auth_token(tok); + } + + foreach(cell, line->databases) + { + AuthToken *tok = lfirst(cell); + + free_auth_token(tok); + } +} + /* * Copy a AuthToken struct into freshly palloc'd memory. */ @@ -661,6 +685,10 @@ is_member(Oid userid, const char *role) /* * Check AuthToken list for a match to role, allowing group names. + * + * Each AuthToken listed is checked one-by-one. Keywords are processed + * first (these cannot have regular expressions), followed by regular + * expressions (if any) and the exact match. */ static bool check_role(const char *role, Oid roleid, List *tokens) @@ -676,8 +704,14 @@ check_role(const char *role, Oid roleid, List *tokens) if (is_member(roleid, tok->string + 1)) return true; } - else if (token_matches(tok, role) || - token_is_keyword(tok, "all")) + else if (token_is_keyword(tok, "all")) + return true; + else if (token_has_regexp(tok)) + { + if (regexec_auth_token(role, tok, 0, NULL) == REG_OKAY) + return true; + } + else if (token_matches(tok, role)) return true; } return false; @@ -685,6 +719,10 @@ check_role(const char *role, Oid roleid, List *tokens) /* * Check to see if db/role combination matches AuthToken list. + * + * Each AuthToken listed is checked one-by-one. Keywords are checked + * first (these cannot have regular expressions), followed by regular + * expressions (if any) and the exact match. */ static bool check_db(const char *dbname, const char *role, Oid roleid, List *tokens) @@ -719,6 +757,11 @@ 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 (token_has_regexp(tok)) + { + if (regexec_auth_token(dbname, tok, 0, NULL) == REG_OKAY) + return true; + } else if (token_matches(tok, dbname)) return true; } @@ -1138,8 +1181,13 @@ 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 regexp for 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. */ @@ -1158,8 +1206,13 @@ 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 regexp 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) @@ -2355,12 +2408,31 @@ load_hba(void) if (!ok) { - /* File contained one or more errors, so bail out */ + /* + * File contained one or more errors, so bail out, first being careful + * to clean up whatever we allocated. Most stuff will go away via + * MemoryContextDelete, but we have to clean up regexes explicitly. + */ + foreach(line, new_parsed_lines) + { + HbaLine *newline = (HbaLine *) lfirst(line); + + free_hba_line(newline); + } MemoryContextDelete(hbacxt); return false; } /* Loaded new file successfully, replace the one we use */ + if (parsed_hba_lines != NIL) + { + foreach(line, parsed_hba_lines) + { + HbaLine *newline = (HbaLine *) lfirst(line); + + free_hba_line(newline); + } + } if (parsed_hba_context != NULL) MemoryContextDelete(parsed_hba_context); parsed_hba_context = hbacxt; diff --git a/src/test/authentication/t/001_password.pl b/src/test/authentication/t/001_password.pl index ea664d18f5..6c0c753b56 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,40 @@ append_to_file( test_conn($node, 'user=md5_role', 'password from pgpass', 0); +# Testing with regular expression for username. The third regexp matches. +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. In this case, the use of +# double quotes is mandatory so as this is not considered as two elements +# of the user name list when parsing pg_hba.conf. +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 regexp 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"};