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"};