Parse pg_ident.conf when it's loaded, keeping it in memory in parsed format.

Similar changes were done to pg_hba.conf earlier already, this commit makes
pg_ident.conf to behave the same as pg_hba.conf.

This has two user-visible effects. First, if pg_ident.conf contains multiple
errors, the whole file is parsed at postmaster startup time and all the
errors are immediately reported. Before this patch, the file was parsed and
the errors were reported only when someone tries to connect using an
authentication method that uses the file, and the parsing stopped on first
error. Second, if you SIGHUP to reload the config files, and the new
pg_ident.conf file contains an error, the error is logged but the old file
stays in effect.

Also, regular expressions in pg_ident.conf are now compiled only once when
the file is loaded, rather than every time the a user is authenticated. That
should speed up authentication if you have a lot of regexps in the file.

Amit Kapila
This commit is contained in:
Heikki Linnakangas 2012-09-21 17:41:22 +03:00
parent 9d5e9730e5
commit 7c45e3a3c6
4 changed files with 216 additions and 97 deletions

View File

@ -73,18 +73,15 @@ static List *parsed_hba_lines = NIL;
static MemoryContext parsed_hba_context = NULL;
/*
* These variables hold the pre-parsed contents of the ident usermap
* configuration file. ident_lines is a triple-nested list of lines, fields
* and tokens, as returned by tokenize_file. There will be one line in
* ident_lines for each (non-empty, non-comment) line of the file. Note there
* will always be at least one field, since blank lines are not entered in the
* data structure. ident_line_nums is an integer list containing the actual
* line number for each line represented in ident_lines. ident_context is
* the memory context holding all this.
* pre-parsed content of ident mapping file: list of IdentLine structs.
* parsed_ident_context is the memory context where it lives.
*
* NOTE: the IdentLine structs can contain pre-compiled regular expressions
* that live outside the memory context. Before destroying or resetting the
* memory context, they need to be expliticly free'd.
*/
static List *ident_lines = NIL;
static List *ident_line_nums = NIL;
static MemoryContext ident_context = NULL;
static List *parsed_ident_lines = NIL;
static MemoryContext parsed_ident_context = NULL;
static MemoryContext tokenize_file(const char *filename, FILE *file,
@ -782,8 +779,7 @@ check_same_host_or_net(SockAddr *raddr, IPCompareMethod method)
(errcode(ERRCODE_CONFIG_FILE_ERROR), \
errmsg("missing entry in file \"%s\" at end of line %d", \
IdentFileName, line_number))); \
*error_p = true; \
return; \
return NULL; \
} \
} while (0);
@ -794,8 +790,7 @@ check_same_host_or_net(SockAddr *raddr, IPCompareMethod method)
errmsg("multiple values in ident field"), \
errcontext("line %d of configuration file \"%s\"", \
line_number, IdentFileName))); \
*error_p = true; \
return; \
return NULL; \
} \
} while (0);
@ -1794,34 +1789,37 @@ load_hba(void)
}
/*
* Process one line from the ident config file.
* Parse one tokenised line from the ident config file and store the result in
* an IdentLine structure, or NULL if parsing fails.
*
* Take the line and compare it to the needed map, pg_role and ident_user.
* *found_p and *error_p are set according to our results.
* The tokenised line is a nested List of fields and tokens.
*
* If ident_user is a regular expression (ie. begins with a slash), it is
* compiled and stored in IdentLine structure.
*
* Note: this function leaks memory when an error occurs. Caller is expected
* to have set a memory context that will be reset if this function returns
* NULL.
*/
static void
parse_ident_usermap(List *line, int line_number, const char *usermap_name,
const char *pg_role, const char *ident_user,
bool case_insensitive, bool *found_p, bool *error_p)
static IdentLine *
parse_ident_line(List *line, int line_number)
{
ListCell *field;
List *tokens;
HbaToken *token;
char *file_map;
char *file_pgrole;
char *file_ident_user;
*found_p = false;
*error_p = false;
IdentLine *parsedline;
Assert(line != NIL);
field = list_head(line);
parsedline = palloc0(sizeof(IdentLine));
parsedline->linenumber = line_number;
/* Get the map token (must exist) */
tokens = lfirst(field);
IDENT_MULTI_VALUE(tokens);
token = linitial(tokens);
file_map = token->string;
parsedline->usermap = pstrdup(token->string);
/* Get the ident user token */
field = lnext(field);
@ -1829,7 +1827,7 @@ parse_ident_usermap(List *line, int line_number, const char *usermap_name,
tokens = lfirst(field);
IDENT_MULTI_VALUE(tokens);
token = linitial(tokens);
file_ident_user = token->string;
parsedline->ident_user = pstrdup(token->string);
/* Get the PG rolename token */
field = lnext(field);
@ -1837,14 +1835,62 @@ parse_ident_usermap(List *line, int line_number, const char *usermap_name,
tokens = lfirst(field);
IDENT_MULTI_VALUE(tokens);
token = linitial(tokens);
file_pgrole = token->string;
parsedline->pg_role = pstrdup(token->string);
if (strcmp(file_map, usermap_name) != 0)
if (parsedline->ident_user[0] == '/')
{
/*
* 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(LOG,
(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
errmsg("invalid regular expression \"%s\": %s",
parsedline->ident_user + 1, errstr)));
pfree(wstr);
return NULL;
}
pfree(wstr);
}
return parsedline;
}
/*
* Process one line from the parsed ident config lines.
*
* Compare input parsed ident line to the needed map, pg_role and ident_user.
* *found_p and *error_p are set according to our results.
*/
static void
check_ident_usermap(IdentLine *identLine, const char *usermap_name,
const char *pg_role, const char *ident_user,
bool case_insensitive, bool *found_p, bool *error_p)
{
*found_p = false;
*error_p = false;
if (strcmp(identLine->usermap, usermap_name) != 0)
/* Line does not match the map name we're looking for, so just abort */
return;
/* Match? */
if (file_ident_user[0] == '/')
if (identLine->ident_user[0] == '/')
{
/*
* When system username starts with a slash, treat it as a regular
@ -1853,41 +1899,16 @@ parse_ident_usermap(List *line, int line_number, const char *usermap_name,
* for \1 in the database username string, if present.
*/
int r;
regex_t re;
regmatch_t matches[2];
pg_wchar *wstr;
int wlen;
char *ofs;
char *regexp_pgrole;
wstr = palloc((strlen(file_ident_user + 1) + 1) * sizeof(pg_wchar));
wlen = pg_mb2wchar_with_len(file_ident_user + 1, wstr, strlen(file_ident_user + 1));
/*
* XXX: Major room for optimization: regexps could be compiled when
* the file is loaded and then re-used in every connection.
*/
r = pg_regcomp(&re, wstr, wlen, REG_ADVANCED, C_COLLATION_OID);
if (r)
{
char errstr[100];
pg_regerror(r, &re, errstr, sizeof(errstr));
ereport(LOG,
(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
errmsg("invalid regular expression \"%s\": %s",
file_ident_user + 1, errstr)));
pfree(wstr);
*error_p = true;
return;
}
pfree(wstr);
wstr = palloc((strlen(ident_user) + 1) * sizeof(pg_wchar));
wlen = pg_mb2wchar_with_len(ident_user, wstr, strlen(ident_user));
r = pg_regexec(&re, wstr, wlen, 0, NULL, 2, matches, 0);
r = pg_regexec(&identLine->re, wstr, wlen, 0, NULL, 2, matches, 0);
if (r)
{
char errstr[100];
@ -1895,21 +1916,20 @@ parse_ident_usermap(List *line, int line_number, const char *usermap_name,
if (r != REG_NOMATCH)
{
/* REG_NOMATCH is not an error, everything else is */
pg_regerror(r, &re, errstr, sizeof(errstr));
pg_regerror(r, &identLine->re, errstr, sizeof(errstr));
ereport(LOG,
(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
errmsg("regular expression match for \"%s\" failed: %s",
file_ident_user + 1, errstr)));
identLine->ident_user + 1, errstr)));
*error_p = true;
}
pfree(wstr);
pg_regfree(&re);
return;
}
pfree(wstr);
if ((ofs = strstr(file_pgrole, "\\1")) != NULL)
if ((ofs = strstr(identLine->pg_role, "\\1")) != NULL)
{
/* substitution of the first argument requested */
if (matches[1].rm_so < 0)
@ -1917,8 +1937,7 @@ parse_ident_usermap(List *line, int line_number, const char *usermap_name,
ereport(LOG,
(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
errmsg("regular expression \"%s\" has no subexpressions as requested by backreference in \"%s\"",
file_ident_user + 1, file_pgrole)));
pg_regfree(&re);
identLine->ident_user + 1, identLine->pg_role)));
*error_p = true;
return;
}
@ -1927,8 +1946,8 @@ parse_ident_usermap(List *line, int line_number, const char *usermap_name,
* length: original length minus length of \1 plus length of match
* plus null terminator
*/
regexp_pgrole = palloc0(strlen(file_pgrole) - 2 + (matches[1].rm_eo - matches[1].rm_so) + 1);
strncpy(regexp_pgrole, file_pgrole, (ofs - file_pgrole));
regexp_pgrole = palloc0(strlen(identLine->pg_role) - 2 + (matches[1].rm_eo - matches[1].rm_so) + 1);
strncpy(regexp_pgrole, identLine->pg_role, (ofs - identLine->pg_role));
memcpy(regexp_pgrole + strlen(regexp_pgrole),
ident_user + matches[1].rm_so,
matches[1].rm_eo - matches[1].rm_so);
@ -1937,11 +1956,9 @@ parse_ident_usermap(List *line, int line_number, const char *usermap_name,
else
{
/* no substitution, so copy the match */
regexp_pgrole = pstrdup(file_pgrole);
regexp_pgrole = pstrdup(identLine->pg_role);
}
pg_regfree(&re);
/*
* now check if the username actually matched what the user is trying
* to connect as
@ -1965,14 +1982,14 @@ parse_ident_usermap(List *line, int line_number, const char *usermap_name,
/* Not regular expression, so make complete match */
if (case_insensitive)
{
if (pg_strcasecmp(file_pgrole, pg_role) == 0 &&
pg_strcasecmp(file_ident_user, ident_user) == 0)
if (pg_strcasecmp(identLine->pg_role, pg_role) == 0 &&
pg_strcasecmp(identLine->ident_user, ident_user) == 0)
*found_p = true;
}
else
{
if (strcmp(file_pgrole, pg_role) == 0 &&
strcmp(file_ident_user, ident_user) == 0)
if (strcmp(identLine->pg_role, pg_role) == 0 &&
strcmp(identLine->ident_user, ident_user) == 0)
*found_p = true;
}
}
@ -2021,13 +2038,12 @@ check_usermap(const char *usermap_name,
}
else
{
ListCell *line_cell,
*num_cell;
ListCell *line_cell;
forboth(line_cell, ident_lines, num_cell, ident_line_nums)
foreach(line_cell, parsed_ident_lines)
{
parse_ident_usermap(lfirst(line_cell), lfirst_int(num_cell),
usermap_name, pg_role, auth_user, case_insensitive,
check_ident_usermap(lfirst(line_cell), usermap_name,
pg_role, auth_user, case_insensitive,
&found_entry, &error);
if (found_entry || error)
break;
@ -2044,21 +2060,26 @@ check_usermap(const char *usermap_name,
/*
* Read the ident config file and populate ident_lines and ident_line_nums.
* Read the ident config file and create a List of IdentLine records for
* the contents.
*
* Like parsed_hba_lines, ident_lines is a triple-nested List of lines, fields
* and tokens.
* This works the same as load_hba(), but for the user config file.
*/
void
bool
load_ident(void)
{
FILE *file;
if (ident_context != NULL)
{
MemoryContextDelete(ident_context);
ident_context = NULL;
}
List *ident_lines = NIL;
List *ident_line_nums = NIL;
ListCell *line_cell,
*num_cell,
*parsed_line_cell;
List *new_parsed_lines = NIL;
bool ok = true;
MemoryContext linecxt;
MemoryContext oldcxt;
MemoryContext ident_context;
IdentLine *newline;
file = AllocateFile(IdentFileName, "r");
if (file == NULL)
@ -2068,13 +2089,80 @@ load_ident(void)
(errcode_for_file_access(),
errmsg("could not open usermap file \"%s\": %m",
IdentFileName)));
return false;
}
else
linecxt = tokenize_file(IdentFileName, file, &ident_lines, &ident_line_nums);
FreeFile(file);
/* Now parse all the lines */
ident_context = AllocSetContextCreate(TopMemoryContext,
"ident parser context",
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_MINSIZE,
ALLOCSET_DEFAULT_MAXSIZE);
oldcxt = MemoryContextSwitchTo(ident_context);
forboth(line_cell, ident_lines, num_cell, ident_line_nums)
{
ident_context = tokenize_file(IdentFileName, file, &ident_lines,
&ident_line_nums);
FreeFile(file);
if ((newline = parse_ident_line(lfirst(line_cell), lfirst_int(num_cell))) == NULL)
{
/*
* Parse error in the file, so indicate there's a problem. Free
* all the memory and regular expressions of lines parsed so far.
*/
foreach(parsed_line_cell, new_parsed_lines)
{
newline = (IdentLine *) lfirst(parsed_line_cell);
if (newline->ident_user[0] == '/')
pg_regfree(&newline->re);
}
MemoryContextReset(ident_context);
new_parsed_lines = NIL;
ok = false;
/*
* Keep parsing the rest of the file so we can report errors on
* more than the first row. Error has already been reported in the
* parsing function, so no need to log it here.
*/
continue;
}
new_parsed_lines = lappend(new_parsed_lines, newline);
}
/* Free tokenizer memory */
MemoryContextDelete(linecxt);
MemoryContextSwitchTo(oldcxt);
if (!ok)
{
/* File contained one or more errors, so bail out */
foreach(parsed_line_cell, new_parsed_lines)
{
newline = (IdentLine *) lfirst(parsed_line_cell);
if (newline->ident_user[0] == '/')
pg_regfree(&newline->re);
}
MemoryContextDelete(ident_context);
return false;
}
/* Loaded new file successfully, replace the one we use */
if (parsed_ident_lines != NULL)
{
foreach(parsed_line_cell, parsed_ident_lines)
{
newline = (IdentLine *) lfirst(parsed_line_cell);
if (newline->ident_user[0] == '/')
pg_regfree(&newline->re);
}
MemoryContextDelete(parsed_ident_context);
}
parsed_ident_context = ident_context;
parsed_ident_lines = new_parsed_lines;
return true;
}

View File

@ -1151,7 +1151,16 @@ PostmasterMain(int argc, char *argv[])
ereport(FATAL,
(errmsg("could not load pg_hba.conf")));
}
load_ident();
if (!load_ident())
{
/*
* We can start up without the IDENT file, although it means that you
* cannot log in using any of the authentication methods that need a
* user name mapping. load_ident() already logged the details of
* error to the log.
*/
}
/*
* Remove old temporary files. At this point there can be no other
@ -2153,7 +2162,9 @@ SIGHUP_handler(SIGNAL_ARGS)
ereport(WARNING,
(errmsg("pg_hba.conf not reloaded")));
load_ident();
if (!load_ident())
ereport(WARNING,
(errmsg("pg_ident.conf not reloaded")));
#ifdef EXEC_BACKEND
/* Update the starting-point file for future children */

View File

@ -197,7 +197,16 @@ PerformAuthentication(Port *port)
ereport(FATAL,
(errmsg("could not load pg_hba.conf")));
}
load_ident();
if (!load_ident())
{
/*
* It is ok to continue if we fail to load the IDENT file, although it
* means that you cannot log in using any of the authentication methods
* that need a user name mapping. load_ident() already logged the
* details of error to the log.
*/
}
#endif
/*

View File

@ -13,6 +13,7 @@
#include "libpq/pqcomm.h" /* pgrminclude ignore */ /* needed for NetBSD */
#include "nodes/pg_list.h"
#include "regex/regex.h"
typedef enum UserAuth
@ -82,11 +83,21 @@ typedef struct HbaLine
int radiusport;
} HbaLine;
typedef struct IdentLine
{
int linenumber;
char *usermap;
char *ident_user;
char *pg_role;
regex_t re;
} IdentLine;
/* kluge to avoid including libpq/libpq-be.h here */
typedef struct Port hbaPort;
extern bool load_hba(void);
extern void load_ident(void);
extern bool load_ident(void);
extern void hba_getauthmethod(hbaPort *port);
extern int check_usermap(const char *usermap_name,
const char *pg_role, const char *auth_user,