mirror of
https://git.postgresql.org/git/postgresql.git
synced 2024-09-27 21:52:02 +02:00
1825 lines
46 KiB
C
1825 lines
46 KiB
C
/*-------------------------------------------------------------------------
|
|
*
|
|
* hba.c
|
|
* Routines to handle host based authentication (that's the scheme
|
|
* wherein you authenticate a user by seeing what IP address the system
|
|
* says he comes from and choosing authentication method based on it).
|
|
*
|
|
* Portions Copyright (c) 1996-2010, PostgreSQL Global Development Group
|
|
* Portions Copyright (c) 1994, Regents of the University of California
|
|
*
|
|
*
|
|
* IDENTIFICATION
|
|
* $PostgreSQL: pgsql/src/backend/libpq/hba.c,v 1.203 2010/03/21 00:17:58 petere Exp $
|
|
*
|
|
*-------------------------------------------------------------------------
|
|
*/
|
|
#include "postgres.h"
|
|
|
|
#include <ctype.h>
|
|
#include <pwd.h>
|
|
#include <fcntl.h>
|
|
#include <sys/param.h>
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <unistd.h>
|
|
|
|
#include "libpq/ip.h"
|
|
#include "libpq/libpq.h"
|
|
#include "regex/regex.h"
|
|
#include "replication/walsender.h"
|
|
#include "storage/fd.h"
|
|
#include "utils/acl.h"
|
|
#include "utils/guc.h"
|
|
#include "utils/lsyscache.h"
|
|
|
|
|
|
#define atooid(x) ((Oid) strtoul((x), NULL, 10))
|
|
#define atoxid(x) ((TransactionId) strtoul((x), NULL, 10))
|
|
|
|
/* This is used to separate values in multi-valued column strings */
|
|
#define MULTI_VALUE_SEP "\001"
|
|
|
|
#define MAX_TOKEN 256
|
|
|
|
/* callback data for check_network_callback */
|
|
typedef struct check_network_data
|
|
{
|
|
IPCompareMethod method; /* test method */
|
|
SockAddr *raddr; /* client's actual address */
|
|
bool result; /* set to true if match */
|
|
} check_network_data;
|
|
|
|
/* pre-parsed content of HBA config file: list of HbaLine structs */
|
|
static List *parsed_hba_lines = NIL;
|
|
|
|
/*
|
|
* These variables hold the pre-parsed contents of the ident usermap
|
|
* configuration file. ident_lines is a list of sublists, one sublist for
|
|
* each (non-empty, non-comment) line of the file. The sublist items are
|
|
* palloc'd strings, one string per token on the line. Note there will always
|
|
* be at least one token, 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.
|
|
*/
|
|
static List *ident_lines = NIL;
|
|
static List *ident_line_nums = NIL;
|
|
|
|
|
|
static void tokenize_file(const char *filename, FILE *file,
|
|
List **lines, List **line_nums);
|
|
static char *tokenize_inc_file(const char *outer_filename,
|
|
const char *inc_filename);
|
|
|
|
/*
|
|
* isblank() exists in the ISO C99 spec, but it's not very portable yet,
|
|
* so provide our own version.
|
|
*/
|
|
bool
|
|
pg_isblank(const char c)
|
|
{
|
|
return c == ' ' || c == '\t' || c == '\r';
|
|
}
|
|
|
|
|
|
/*
|
|
* Grab one token out of fp. Tokens are strings of non-blank
|
|
* characters bounded by blank characters, commas, beginning of line, and
|
|
* end of line. Blank means space or tab. Tokens can be delimited by
|
|
* double quotes (this allows the inclusion of blanks, but not newlines).
|
|
*
|
|
* The token, if any, is returned at *buf (a buffer of size bufsz).
|
|
* Also, we set *initial_quote to indicate whether there was quoting before
|
|
* the first character. (We use that to prevent "@x" from being treated
|
|
* as a file inclusion request. Note that @"x" should be so treated;
|
|
* we want to allow that to support embedded spaces in file paths.)
|
|
*
|
|
* If successful: store null-terminated token at *buf and return TRUE.
|
|
* If no more tokens on line: set *buf = '\0' and return FALSE.
|
|
*
|
|
* Leave file positioned at the character immediately after the token or EOF,
|
|
* whichever comes first. If no more tokens on line, position the file to the
|
|
* beginning of the next line or EOF, whichever comes first.
|
|
*
|
|
* Handle comments. Treat unquoted keywords that might be role names or
|
|
* database names specially, by appending a newline to them. Also, when
|
|
* a token is terminated by a comma, the comma is included in the returned
|
|
* token.
|
|
*/
|
|
static bool
|
|
next_token(FILE *fp, char *buf, int bufsz, bool *initial_quote)
|
|
{
|
|
int c;
|
|
char *start_buf = buf;
|
|
char *end_buf = buf + (bufsz - 2);
|
|
bool in_quote = false;
|
|
bool was_quote = false;
|
|
bool saw_quote = false;
|
|
|
|
/* end_buf reserves two bytes to ensure we can append \n and \0 */
|
|
Assert(end_buf > start_buf);
|
|
|
|
*initial_quote = false;
|
|
|
|
/* Move over initial whitespace and commas */
|
|
while ((c = getc(fp)) != EOF && (pg_isblank(c) || c == ','))
|
|
;
|
|
|
|
if (c == EOF || c == '\n')
|
|
{
|
|
*buf = '\0';
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Build a token in buf of next characters up to EOF, EOL, unquoted comma,
|
|
* or unquoted whitespace.
|
|
*/
|
|
while (c != EOF && c != '\n' &&
|
|
(!pg_isblank(c) || in_quote))
|
|
{
|
|
/* skip comments to EOL */
|
|
if (c == '#' && !in_quote)
|
|
{
|
|
while ((c = getc(fp)) != EOF && c != '\n')
|
|
;
|
|
/* If only comment, consume EOL too; return EOL */
|
|
if (c != EOF && buf == start_buf)
|
|
c = getc(fp);
|
|
break;
|
|
}
|
|
|
|
if (buf >= end_buf)
|
|
{
|
|
*buf = '\0';
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("authentication file token too long, skipping: \"%s\"",
|
|
start_buf)));
|
|
/* Discard remainder of line */
|
|
while ((c = getc(fp)) != EOF && c != '\n')
|
|
;
|
|
break;
|
|
}
|
|
|
|
if (c != '"' || was_quote)
|
|
*buf++ = c;
|
|
|
|
/* We pass back the comma so the caller knows there is more */
|
|
if (c == ',' && !in_quote)
|
|
break;
|
|
|
|
/* Literal double-quote is two double-quotes */
|
|
if (in_quote && c == '"')
|
|
was_quote = !was_quote;
|
|
else
|
|
was_quote = false;
|
|
|
|
if (c == '"')
|
|
{
|
|
in_quote = !in_quote;
|
|
saw_quote = true;
|
|
if (buf == start_buf)
|
|
*initial_quote = true;
|
|
}
|
|
|
|
c = getc(fp);
|
|
}
|
|
|
|
/*
|
|
* Put back the char right after the token (critical in case it is EOL,
|
|
* since we need to detect end-of-line at next call).
|
|
*/
|
|
if (c != EOF)
|
|
ungetc(c, fp);
|
|
|
|
*buf = '\0';
|
|
|
|
if (!saw_quote &&
|
|
(strcmp(start_buf, "all") == 0 ||
|
|
strcmp(start_buf, "sameuser") == 0 ||
|
|
strcmp(start_buf, "samegroup") == 0 ||
|
|
strcmp(start_buf, "samerole") == 0 ||
|
|
strcmp(start_buf, "replication") == 0))
|
|
{
|
|
/* append newline to a magical keyword */
|
|
*buf++ = '\n';
|
|
*buf = '\0';
|
|
}
|
|
|
|
return (saw_quote || buf > start_buf);
|
|
}
|
|
|
|
/*
|
|
* Tokenize file and handle file inclusion and comma lists. We have
|
|
* to break apart the commas to expand any file names then
|
|
* reconstruct with commas.
|
|
*
|
|
* The result is a palloc'd string, or NULL if we have reached EOL.
|
|
*/
|
|
static char *
|
|
next_token_expand(const char *filename, FILE *file)
|
|
{
|
|
char buf[MAX_TOKEN];
|
|
char *comma_str = pstrdup("");
|
|
bool got_something = false;
|
|
bool trailing_comma;
|
|
bool initial_quote;
|
|
char *incbuf;
|
|
int needed;
|
|
|
|
do
|
|
{
|
|
if (!next_token(file, buf, sizeof(buf), &initial_quote))
|
|
break;
|
|
|
|
got_something = true;
|
|
|
|
if (strlen(buf) > 0 && buf[strlen(buf) - 1] == ',')
|
|
{
|
|
trailing_comma = true;
|
|
buf[strlen(buf) - 1] = '\0';
|
|
}
|
|
else
|
|
trailing_comma = false;
|
|
|
|
/* Is this referencing a file? */
|
|
if (!initial_quote && buf[0] == '@' && buf[1] != '\0')
|
|
incbuf = tokenize_inc_file(filename, buf + 1);
|
|
else
|
|
incbuf = pstrdup(buf);
|
|
|
|
needed = strlen(comma_str) + strlen(incbuf) + 1;
|
|
if (trailing_comma)
|
|
needed++;
|
|
comma_str = repalloc(comma_str, needed);
|
|
strcat(comma_str, incbuf);
|
|
if (trailing_comma)
|
|
strcat(comma_str, MULTI_VALUE_SEP);
|
|
pfree(incbuf);
|
|
} while (trailing_comma);
|
|
|
|
if (!got_something)
|
|
{
|
|
pfree(comma_str);
|
|
return NULL;
|
|
}
|
|
|
|
return comma_str;
|
|
}
|
|
|
|
|
|
/*
|
|
* Free memory used by lines/tokens (i.e., structure built by tokenize_file)
|
|
*/
|
|
static void
|
|
free_lines(List **lines, List **line_nums)
|
|
{
|
|
/*
|
|
* Either both must be non-NULL, or both must be NULL
|
|
*/
|
|
Assert((*lines != NIL && *line_nums != NIL) ||
|
|
(*lines == NIL && *line_nums == NIL));
|
|
|
|
if (*lines)
|
|
{
|
|
/*
|
|
* "lines" is a list of lists; each of those sublists consists of
|
|
* palloc'ed tokens, so we want to free each pointed-to token in a
|
|
* sublist, followed by the sublist itself, and finally the whole
|
|
* list.
|
|
*/
|
|
ListCell *line;
|
|
|
|
foreach(line, *lines)
|
|
{
|
|
List *ln = lfirst(line);
|
|
ListCell *token;
|
|
|
|
foreach(token, ln)
|
|
pfree(lfirst(token));
|
|
/* free the sublist structure itself */
|
|
list_free(ln);
|
|
}
|
|
/* free the list structure itself */
|
|
list_free(*lines);
|
|
/* clear the static variable */
|
|
*lines = NIL;
|
|
}
|
|
|
|
if (*line_nums)
|
|
{
|
|
list_free(*line_nums);
|
|
*line_nums = NIL;
|
|
}
|
|
}
|
|
|
|
|
|
static char *
|
|
tokenize_inc_file(const char *outer_filename,
|
|
const char *inc_filename)
|
|
{
|
|
char *inc_fullname;
|
|
FILE *inc_file;
|
|
List *inc_lines;
|
|
List *inc_line_nums;
|
|
ListCell *line;
|
|
char *comma_str;
|
|
|
|
if (is_absolute_path(inc_filename))
|
|
{
|
|
/* absolute path is taken as-is */
|
|
inc_fullname = pstrdup(inc_filename);
|
|
}
|
|
else
|
|
{
|
|
/* relative path is relative to dir of calling file */
|
|
inc_fullname = (char *) palloc(strlen(outer_filename) + 1 +
|
|
strlen(inc_filename) + 1);
|
|
strcpy(inc_fullname, outer_filename);
|
|
get_parent_directory(inc_fullname);
|
|
join_path_components(inc_fullname, inc_fullname, inc_filename);
|
|
canonicalize_path(inc_fullname);
|
|
}
|
|
|
|
inc_file = AllocateFile(inc_fullname, "r");
|
|
if (inc_file == NULL)
|
|
{
|
|
ereport(LOG,
|
|
(errcode_for_file_access(),
|
|
errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m",
|
|
inc_filename, inc_fullname)));
|
|
pfree(inc_fullname);
|
|
|
|
/* return single space, it matches nothing */
|
|
return pstrdup(" ");
|
|
}
|
|
|
|
/* There is possible recursion here if the file contains @ */
|
|
tokenize_file(inc_fullname, inc_file, &inc_lines, &inc_line_nums);
|
|
|
|
FreeFile(inc_file);
|
|
pfree(inc_fullname);
|
|
|
|
/* Create comma-separated string from List */
|
|
comma_str = pstrdup("");
|
|
foreach(line, inc_lines)
|
|
{
|
|
List *token_list = (List *) lfirst(line);
|
|
ListCell *token;
|
|
|
|
foreach(token, token_list)
|
|
{
|
|
int oldlen = strlen(comma_str);
|
|
int needed;
|
|
|
|
needed = oldlen + strlen(lfirst(token)) + 1;
|
|
if (oldlen > 0)
|
|
needed++;
|
|
comma_str = repalloc(comma_str, needed);
|
|
if (oldlen > 0)
|
|
strcat(comma_str, MULTI_VALUE_SEP);
|
|
strcat(comma_str, lfirst(token));
|
|
}
|
|
}
|
|
|
|
free_lines(&inc_lines, &inc_line_nums);
|
|
|
|
/* if file is empty, return single space rather than empty string */
|
|
if (strlen(comma_str) == 0)
|
|
{
|
|
pfree(comma_str);
|
|
return pstrdup(" ");
|
|
}
|
|
|
|
return comma_str;
|
|
}
|
|
|
|
|
|
/*
|
|
* Tokenize the given file, storing the resulting data into two lists:
|
|
* a list of sublists, each sublist containing the tokens in a line of
|
|
* the file, and a list of line numbers.
|
|
*
|
|
* filename must be the absolute path to the target file.
|
|
*/
|
|
static void
|
|
tokenize_file(const char *filename, FILE *file,
|
|
List **lines, List **line_nums)
|
|
{
|
|
List *current_line = NIL;
|
|
int line_number = 1;
|
|
char *buf;
|
|
|
|
*lines = *line_nums = NIL;
|
|
|
|
while (!feof(file) && !ferror(file))
|
|
{
|
|
buf = next_token_expand(filename, file);
|
|
|
|
/* add token to list, unless we are at EOL or comment start */
|
|
if (buf)
|
|
{
|
|
if (current_line == NIL)
|
|
{
|
|
/* make a new line List, record its line number */
|
|
current_line = lappend(current_line, buf);
|
|
*lines = lappend(*lines, current_line);
|
|
*line_nums = lappend_int(*line_nums, line_number);
|
|
}
|
|
else
|
|
{
|
|
/* append token to current line's list */
|
|
current_line = lappend(current_line, buf);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
/* we are at real or logical EOL, so force a new line List */
|
|
current_line = NIL;
|
|
/* Advance line number whenever we reach EOL */
|
|
line_number++;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Does user belong to role?
|
|
*
|
|
* userid is the OID of the role given as the attempted login identifier.
|
|
* We check to see if it is a member of the specified role name.
|
|
*/
|
|
static bool
|
|
is_member(Oid userid, const char *role)
|
|
{
|
|
Oid roleid;
|
|
|
|
if (!OidIsValid(userid))
|
|
return false; /* if user not exist, say "no" */
|
|
|
|
roleid = get_roleid(role);
|
|
|
|
if (!OidIsValid(roleid))
|
|
return false; /* if target role not exist, say "no" */
|
|
|
|
/* See if user is directly or indirectly a member of role */
|
|
return is_member_of_role(userid, roleid);
|
|
}
|
|
|
|
/*
|
|
* Check comma-separated list for a match to role, allowing group names.
|
|
*
|
|
* NB: param_str is destructively modified! In current usage, this is
|
|
* okay only because this code is run after forking off from the postmaster,
|
|
* and so it doesn't matter that we clobber the stored hba info.
|
|
*/
|
|
static bool
|
|
check_role(const char *role, Oid roleid, char *param_str)
|
|
{
|
|
char *tok;
|
|
|
|
for (tok = strtok(param_str, MULTI_VALUE_SEP);
|
|
tok != NULL;
|
|
tok = strtok(NULL, MULTI_VALUE_SEP))
|
|
{
|
|
if (tok[0] == '+')
|
|
{
|
|
if (is_member(roleid, tok + 1))
|
|
return true;
|
|
}
|
|
else if (strcmp(tok, role) == 0 ||
|
|
strcmp(tok, "all\n") == 0)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Check to see if db/role combination matches param string.
|
|
*
|
|
* NB: param_str is destructively modified! In current usage, this is
|
|
* okay only because this code is run after forking off from the postmaster,
|
|
* and so it doesn't matter that we clobber the stored hba info.
|
|
*/
|
|
static bool
|
|
check_db(const char *dbname, const char *role, Oid roleid, char *param_str)
|
|
{
|
|
char *tok;
|
|
|
|
for (tok = strtok(param_str, MULTI_VALUE_SEP);
|
|
tok != NULL;
|
|
tok = strtok(NULL, MULTI_VALUE_SEP))
|
|
{
|
|
if (strcmp(tok, "all\n") == 0)
|
|
return true;
|
|
else if (strcmp(tok, "sameuser\n") == 0)
|
|
{
|
|
if (strcmp(dbname, role) == 0)
|
|
return true;
|
|
}
|
|
else if (strcmp(tok, "samegroup\n") == 0 ||
|
|
strcmp(tok, "samerole\n") == 0)
|
|
{
|
|
if (is_member(roleid, dbname))
|
|
return true;
|
|
}
|
|
else if (strcmp(tok, "replication\n") == 0 &&
|
|
am_walsender)
|
|
return true;
|
|
else if (strcmp(tok, dbname) == 0)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/*
|
|
* Check to see if a connecting IP matches the given address and netmask.
|
|
*/
|
|
static bool
|
|
check_ip(SockAddr *raddr, struct sockaddr * addr, struct sockaddr * mask)
|
|
{
|
|
if (raddr->addr.ss_family == addr->sa_family)
|
|
{
|
|
/* Same address family */
|
|
if (!pg_range_sockaddr(&raddr->addr,
|
|
(struct sockaddr_storage *) addr,
|
|
(struct sockaddr_storage *) mask))
|
|
return false;
|
|
}
|
|
#ifdef HAVE_IPV6
|
|
else if (addr->sa_family == AF_INET &&
|
|
raddr->addr.ss_family == AF_INET6)
|
|
{
|
|
/*
|
|
* If we're connected on IPv6 but the file specifies an IPv4 address
|
|
* to match against, promote the latter to an IPv6 address before
|
|
* trying to match the client's address.
|
|
*/
|
|
struct sockaddr_storage addrcopy,
|
|
maskcopy;
|
|
|
|
memcpy(&addrcopy, &addr, sizeof(addrcopy));
|
|
memcpy(&maskcopy, &mask, sizeof(maskcopy));
|
|
pg_promote_v4_to_v6_addr(&addrcopy);
|
|
pg_promote_v4_to_v6_mask(&maskcopy);
|
|
|
|
if (!pg_range_sockaddr(&raddr->addr, &addrcopy, &maskcopy))
|
|
return false;
|
|
}
|
|
#endif /* HAVE_IPV6 */
|
|
else
|
|
{
|
|
/* Wrong address family, no IPV6 */
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* pg_foreach_ifaddr callback: does client addr match this machine interface?
|
|
*/
|
|
static void
|
|
check_network_callback(struct sockaddr * addr, struct sockaddr * netmask,
|
|
void *cb_data)
|
|
{
|
|
check_network_data *cn = (check_network_data *) cb_data;
|
|
struct sockaddr_storage mask;
|
|
|
|
/* Already found a match? */
|
|
if (cn->result)
|
|
return;
|
|
|
|
if (cn->method == ipCmpSameHost)
|
|
{
|
|
/* Make an all-ones netmask of appropriate length for family */
|
|
pg_sockaddr_cidr_mask(&mask, NULL, addr->sa_family);
|
|
cn->result = check_ip(cn->raddr, addr, (struct sockaddr *) & mask);
|
|
}
|
|
else
|
|
{
|
|
/* Use the netmask of the interface itself */
|
|
cn->result = check_ip(cn->raddr, addr, netmask);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Use pg_foreach_ifaddr to check a samehost or samenet match
|
|
*/
|
|
static bool
|
|
check_same_host_or_net(SockAddr *raddr, IPCompareMethod method)
|
|
{
|
|
check_network_data cn;
|
|
|
|
cn.method = method;
|
|
cn.raddr = raddr;
|
|
cn.result = false;
|
|
|
|
errno = 0;
|
|
if (pg_foreach_ifaddr(check_network_callback, &cn) < 0)
|
|
{
|
|
elog(LOG, "error enumerating network interfaces: %m");
|
|
return false;
|
|
}
|
|
|
|
return cn.result;
|
|
}
|
|
|
|
|
|
/*
|
|
* Macros used to check and report on invalid configuration options.
|
|
* INVALID_AUTH_OPTION = reports when an option is specified for a method where it's
|
|
* not supported.
|
|
* REQUIRE_AUTH_OPTION = same as INVALID_AUTH_OPTION, except it also checks if the
|
|
* method is actually the one specified. Used as a shortcut when
|
|
* the option is only valid for one authentication method.
|
|
* MANDATORY_AUTH_ARG = check if a required option is set for an authentication method,
|
|
* reporting error if it's not.
|
|
*/
|
|
#define INVALID_AUTH_OPTION(optname, validmethods) do {\
|
|
ereport(LOG, \
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR), \
|
|
/* translator: the second %s is a list of auth methods */ \
|
|
errmsg("authentication option \"%s\" is only valid for authentication methods %s", \
|
|
optname, _(validmethods)), \
|
|
errcontext("line %d of configuration file \"%s\"", \
|
|
line_num, HbaFileName))); \
|
|
return false; \
|
|
} while (0);
|
|
|
|
#define REQUIRE_AUTH_OPTION(methodval, optname, validmethods) do {\
|
|
if (parsedline->auth_method != methodval) \
|
|
INVALID_AUTH_OPTION(optname, validmethods); \
|
|
} while (0);
|
|
|
|
#define MANDATORY_AUTH_ARG(argvar, argname, authname) do {\
|
|
if (argvar == NULL) {\
|
|
ereport(LOG, \
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR), \
|
|
errmsg("authentication method \"%s\" requires argument \"%s\" to be set", \
|
|
authname, argname), \
|
|
errcontext("line %d of configuration file \"%s\"", \
|
|
line_num, HbaFileName))); \
|
|
return false; \
|
|
} \
|
|
} while (0);
|
|
|
|
|
|
/*
|
|
* Parse one line in the hba config file and store the result in
|
|
* a HbaLine structure.
|
|
*/
|
|
static bool
|
|
parse_hba_line(List *line, int line_num, HbaLine *parsedline)
|
|
{
|
|
char *token;
|
|
struct addrinfo *gai_result;
|
|
struct addrinfo hints;
|
|
int ret;
|
|
char *cidr_slash;
|
|
char *unsupauth;
|
|
ListCell *line_item;
|
|
|
|
line_item = list_head(line);
|
|
|
|
parsedline->linenumber = line_num;
|
|
|
|
/* Check the record type. */
|
|
token = lfirst(line_item);
|
|
if (strcmp(token, "local") == 0)
|
|
{
|
|
parsedline->conntype = ctLocal;
|
|
}
|
|
else if (strcmp(token, "host") == 0
|
|
|| strcmp(token, "hostssl") == 0
|
|
|| strcmp(token, "hostnossl") == 0)
|
|
{
|
|
|
|
if (token[4] == 's') /* "hostssl" */
|
|
{
|
|
#ifdef USE_SSL
|
|
parsedline->conntype = ctHostSSL;
|
|
#else
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("hostssl not supported on this platform"),
|
|
errhint("compile with --enable-ssl to use SSL connections"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
#endif
|
|
}
|
|
#ifdef USE_SSL
|
|
else if (token[4] == 'n') /* "hostnossl" */
|
|
{
|
|
parsedline->conntype = ctHostNoSSL;
|
|
}
|
|
#endif
|
|
else
|
|
{
|
|
/* "host", or "hostnossl" and SSL support not built in */
|
|
parsedline->conntype = ctHost;
|
|
}
|
|
} /* record type */
|
|
else
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("invalid connection type \"%s\"",
|
|
token),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
|
|
/* Get the database. */
|
|
line_item = lnext(line_item);
|
|
if (!line_item)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("end-of-line before database specification"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
parsedline->database = pstrdup(lfirst(line_item));
|
|
|
|
/* Get the role. */
|
|
line_item = lnext(line_item);
|
|
if (!line_item)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("end-of-line before role specification"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
parsedline->role = pstrdup(lfirst(line_item));
|
|
|
|
if (parsedline->conntype != ctLocal)
|
|
{
|
|
/* Read the IP address field. (with or without CIDR netmask) */
|
|
line_item = lnext(line_item);
|
|
if (!line_item)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("end-of-line before IP address specification"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
token = lfirst(line_item);
|
|
|
|
/* Is it equal to 'samehost' or 'samenet'? */
|
|
if (strcmp(token, "samehost") == 0)
|
|
{
|
|
/* Any IP on this host is allowed to connect */
|
|
parsedline->ip_cmp_method = ipCmpSameHost;
|
|
}
|
|
else if (strcmp(token, "samenet") == 0)
|
|
{
|
|
/* Any IP on the host's subnets is allowed to connect */
|
|
parsedline->ip_cmp_method = ipCmpSameNet;
|
|
}
|
|
else
|
|
{
|
|
/* IP and netmask are specified */
|
|
parsedline->ip_cmp_method = ipCmpMask;
|
|
|
|
/* need a modifiable copy of token */
|
|
token = pstrdup(token);
|
|
|
|
/* Check if it has a CIDR suffix and if so isolate it */
|
|
cidr_slash = strchr(token, '/');
|
|
if (cidr_slash)
|
|
*cidr_slash = '\0';
|
|
|
|
/* Get the IP address either way */
|
|
hints.ai_flags = AI_NUMERICHOST;
|
|
hints.ai_family = PF_UNSPEC;
|
|
hints.ai_socktype = 0;
|
|
hints.ai_protocol = 0;
|
|
hints.ai_addrlen = 0;
|
|
hints.ai_canonname = NULL;
|
|
hints.ai_addr = NULL;
|
|
hints.ai_next = NULL;
|
|
|
|
ret = pg_getaddrinfo_all(token, NULL, &hints, &gai_result);
|
|
if (ret || !gai_result)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("invalid IP address \"%s\": %s",
|
|
token, gai_strerror(ret)),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
if (gai_result)
|
|
pg_freeaddrinfo_all(hints.ai_family, gai_result);
|
|
pfree(token);
|
|
return false;
|
|
}
|
|
|
|
memcpy(&parsedline->addr, gai_result->ai_addr,
|
|
gai_result->ai_addrlen);
|
|
pg_freeaddrinfo_all(hints.ai_family, gai_result);
|
|
|
|
/* Get the netmask */
|
|
if (cidr_slash)
|
|
{
|
|
if (pg_sockaddr_cidr_mask(&parsedline->mask, cidr_slash + 1,
|
|
parsedline->addr.ss_family) < 0)
|
|
{
|
|
*cidr_slash = '/'; /* restore token for message */
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("invalid CIDR mask in address \"%s\"",
|
|
token),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
pfree(token);
|
|
return false;
|
|
}
|
|
pfree(token);
|
|
}
|
|
else
|
|
{
|
|
/* Read the mask field. */
|
|
pfree(token);
|
|
line_item = lnext(line_item);
|
|
if (!line_item)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("end-of-line before netmask specification"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
token = lfirst(line_item);
|
|
|
|
ret = pg_getaddrinfo_all(token, NULL, &hints, &gai_result);
|
|
if (ret || !gai_result)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("invalid IP mask \"%s\": %s",
|
|
token, gai_strerror(ret)),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
if (gai_result)
|
|
pg_freeaddrinfo_all(hints.ai_family, gai_result);
|
|
return false;
|
|
}
|
|
|
|
memcpy(&parsedline->mask, gai_result->ai_addr,
|
|
gai_result->ai_addrlen);
|
|
pg_freeaddrinfo_all(hints.ai_family, gai_result);
|
|
|
|
if (parsedline->addr.ss_family != parsedline->mask.ss_family)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("IP address and mask do not match in file \"%s\" line %d",
|
|
HbaFileName, line_num)));
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
} /* != ctLocal */
|
|
|
|
/* Get the authentication method */
|
|
line_item = lnext(line_item);
|
|
if (!line_item)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("end-of-line before authentication method"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
token = lfirst(line_item);
|
|
|
|
unsupauth = NULL;
|
|
if (strcmp(token, "trust") == 0)
|
|
parsedline->auth_method = uaTrust;
|
|
else if (strcmp(token, "ident") == 0)
|
|
parsedline->auth_method = uaIdent;
|
|
else if (strcmp(token, "password") == 0)
|
|
parsedline->auth_method = uaPassword;
|
|
else if (strcmp(token, "krb5") == 0)
|
|
#ifdef KRB5
|
|
parsedline->auth_method = uaKrb5;
|
|
#else
|
|
unsupauth = "krb5";
|
|
#endif
|
|
else if (strcmp(token, "gss") == 0)
|
|
#ifdef ENABLE_GSS
|
|
parsedline->auth_method = uaGSS;
|
|
#else
|
|
unsupauth = "gss";
|
|
#endif
|
|
else if (strcmp(token, "sspi") == 0)
|
|
#ifdef ENABLE_SSPI
|
|
parsedline->auth_method = uaSSPI;
|
|
#else
|
|
unsupauth = "sspi";
|
|
#endif
|
|
else if (strcmp(token, "reject") == 0)
|
|
parsedline->auth_method = uaReject;
|
|
else if (strcmp(token, "md5") == 0)
|
|
{
|
|
if (Db_user_namespace)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled")));
|
|
return false;
|
|
}
|
|
parsedline->auth_method = uaMD5;
|
|
}
|
|
else if (strcmp(token, "pam") == 0)
|
|
#ifdef USE_PAM
|
|
parsedline->auth_method = uaPAM;
|
|
#else
|
|
unsupauth = "pam";
|
|
#endif
|
|
else if (strcmp(token, "ldap") == 0)
|
|
#ifdef USE_LDAP
|
|
parsedline->auth_method = uaLDAP;
|
|
#else
|
|
unsupauth = "ldap";
|
|
#endif
|
|
else if (strcmp(token, "cert") == 0)
|
|
#ifdef USE_SSL
|
|
parsedline->auth_method = uaCert;
|
|
#else
|
|
unsupauth = "cert";
|
|
#endif
|
|
else if (strcmp(token, "radius") == 0)
|
|
parsedline->auth_method = uaRADIUS;
|
|
else
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("invalid authentication method \"%s\"",
|
|
token),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
|
|
if (unsupauth)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("invalid authentication method \"%s\": not supported on this platform",
|
|
token),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
|
|
/* Invalid authentication combinations */
|
|
if (parsedline->conntype == ctLocal &&
|
|
parsedline->auth_method == uaKrb5)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("krb5 authentication is not supported on local sockets"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
|
|
if (parsedline->conntype == ctLocal &&
|
|
parsedline->auth_method == uaGSS)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("gssapi authentication is not supported on local sockets"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
/*
|
|
* SSPI authentication can never be enabled on ctLocal connections, because
|
|
* it's only supported on Windows, where ctLocal isn't supported.
|
|
*/
|
|
|
|
|
|
if (parsedline->conntype != ctHostSSL &&
|
|
parsedline->auth_method == uaCert)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("cert authentication is only supported on hostssl connections"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
|
|
/* Parse remaining arguments */
|
|
while ((line_item = lnext(line_item)) != NULL)
|
|
{
|
|
char *c;
|
|
|
|
token = lfirst(line_item);
|
|
|
|
c = strchr(token, '=');
|
|
if (c == NULL)
|
|
{
|
|
/*
|
|
* Got something that's not a name=value pair.
|
|
*/
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("authentication option not in name=value format: %s", token),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
*c++ = '\0'; /* token now holds "name", c holds "value" */
|
|
if (strcmp(token, "map") == 0)
|
|
{
|
|
if (parsedline->auth_method != uaIdent &&
|
|
parsedline->auth_method != uaKrb5 &&
|
|
parsedline->auth_method != uaGSS &&
|
|
parsedline->auth_method != uaSSPI &&
|
|
parsedline->auth_method != uaCert)
|
|
INVALID_AUTH_OPTION("map", gettext_noop("ident, krb5, gssapi, sspi and cert"));
|
|
parsedline->usermap = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "clientcert") == 0)
|
|
{
|
|
/*
|
|
* Since we require ctHostSSL, this really can never happen on
|
|
* non-SSL-enabled builds, so don't bother checking for
|
|
* USE_SSL.
|
|
*/
|
|
if (parsedline->conntype != ctHostSSL)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("clientcert can only be configured for \"hostssl\" rows"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
if (strcmp(c, "1") == 0)
|
|
{
|
|
if (!secure_loaded_verify_locations())
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("client certificates can only be checked if a root certificate store is available"),
|
|
errdetail("make sure the root certificate store is present and readable"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
parsedline->clientcert = true;
|
|
}
|
|
else
|
|
{
|
|
if (parsedline->auth_method == uaCert)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("clientcert can not be set to 0 when using \"cert\" authentication"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
parsedline->clientcert = false;
|
|
}
|
|
}
|
|
else if (strcmp(token, "pamservice") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaPAM, "pamservice", "pam");
|
|
parsedline->pamservice = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "ldaptls") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaLDAP, "ldaptls", "ldap");
|
|
if (strcmp(c, "1") == 0)
|
|
parsedline->ldaptls = true;
|
|
else
|
|
parsedline->ldaptls = false;
|
|
}
|
|
else if (strcmp(token, "ldapserver") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap");
|
|
parsedline->ldapserver = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "ldapport") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaLDAP, "ldapport", "ldap");
|
|
parsedline->ldapport = atoi(c);
|
|
if (parsedline->ldapport == 0)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("invalid LDAP port number: \"%s\"", c),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
}
|
|
else if (strcmp(token, "ldapbinddn") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbinddn", "ldap");
|
|
parsedline->ldapbinddn = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "ldapbindpasswd") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbindpasswd", "ldap");
|
|
parsedline->ldapbindpasswd = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "ldapsearchattribute") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap");
|
|
parsedline->ldapsearchattribute = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "ldapbasedn") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap");
|
|
parsedline->ldapbasedn = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "ldapprefix") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaLDAP, "ldapprefix", "ldap");
|
|
parsedline->ldapprefix = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "ldapsuffix") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaLDAP, "ldapsuffix", "ldap");
|
|
parsedline->ldapsuffix = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "krb_server_hostname") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaKrb5, "krb_server_hostname", "krb5");
|
|
parsedline->krb_server_hostname = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "krb_realm") == 0)
|
|
{
|
|
if (parsedline->auth_method != uaKrb5 &&
|
|
parsedline->auth_method != uaGSS &&
|
|
parsedline->auth_method != uaSSPI)
|
|
INVALID_AUTH_OPTION("krb_realm", gettext_noop("krb5, gssapi and sspi"));
|
|
parsedline->krb_realm = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "include_realm") == 0)
|
|
{
|
|
if (parsedline->auth_method != uaKrb5 &&
|
|
parsedline->auth_method != uaGSS &&
|
|
parsedline->auth_method != uaSSPI)
|
|
INVALID_AUTH_OPTION("include_realm", gettext_noop("krb5, gssapi and sspi"));
|
|
if (strcmp(c, "1") == 0)
|
|
parsedline->include_realm = true;
|
|
else
|
|
parsedline->include_realm = false;
|
|
}
|
|
else if (strcmp(token, "radiusserver") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaRADIUS, "radiusserver", "radius");
|
|
|
|
MemSet(&hints, 0, sizeof(hints));
|
|
hints.ai_socktype = SOCK_DGRAM;
|
|
hints.ai_family = AF_UNSPEC;
|
|
|
|
ret = pg_getaddrinfo_all(c, NULL, &hints, &gai_result);
|
|
if (ret || !gai_result)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("could not translate RADIUS server name \"%s\" to address: %s",
|
|
c, gai_strerror(ret)),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
if (gai_result)
|
|
pg_freeaddrinfo_all(hints.ai_family, gai_result);
|
|
return false;
|
|
}
|
|
pg_freeaddrinfo_all(hints.ai_family, gai_result);
|
|
parsedline->radiusserver = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "radiusport") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaRADIUS, "radiusport", "radius");
|
|
parsedline->radiusport = atoi(c);
|
|
if (parsedline->radiusport == 0)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("invalid RADIUS port number: \"%s\"", c),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
}
|
|
else if (strcmp(token, "radiussecret") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecret", "radius");
|
|
parsedline->radiussecret = pstrdup(c);
|
|
}
|
|
else if (strcmp(token, "radiusidentifier") == 0)
|
|
{
|
|
REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifier", "radius");
|
|
parsedline->radiusidentifier = pstrdup(c);
|
|
}
|
|
else
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("unknown authentication option name: \"%s\"", token),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Check if the selected authentication method has any mandatory arguments
|
|
* that are not set.
|
|
*/
|
|
if (parsedline->auth_method == uaLDAP)
|
|
{
|
|
MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap");
|
|
|
|
/*
|
|
* LDAP can operate in two modes: either with a direct bind, using
|
|
* ldapprefix and ldapsuffix, or using a search+bind, using
|
|
* ldapbasedn, ldapbinddn, ldapbindpasswd and ldapsearchattribute.
|
|
* Disallow mixing these parameters.
|
|
*/
|
|
if (parsedline->ldapprefix || parsedline->ldapsuffix)
|
|
{
|
|
if (parsedline->ldapbasedn ||
|
|
parsedline->ldapbinddn ||
|
|
parsedline->ldapbindpasswd ||
|
|
parsedline->ldapsearchattribute)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, or ldapsearchattribute together with ldapprefix"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
}
|
|
else if (!parsedline->ldapbasedn)
|
|
{
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set"),
|
|
errcontext("line %d of configuration file \"%s\"",
|
|
line_num, HbaFileName)));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (parsedline->auth_method == uaRADIUS)
|
|
{
|
|
MANDATORY_AUTH_ARG(parsedline->radiusserver, "radiusserver", "radius");
|
|
MANDATORY_AUTH_ARG(parsedline->radiussecret, "radiussecret", "radius");
|
|
}
|
|
|
|
/*
|
|
* Enforce any parameters implied by other settings.
|
|
*/
|
|
if (parsedline->auth_method == uaCert)
|
|
{
|
|
parsedline->clientcert = true;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Scan the (pre-parsed) hba file line by line, looking for a match
|
|
* to the port's connection request.
|
|
*/
|
|
static bool
|
|
check_hba(hbaPort *port)
|
|
{
|
|
Oid roleid;
|
|
ListCell *line;
|
|
HbaLine *hba;
|
|
|
|
/* Get the target role's OID. Note we do not error out for bad role. */
|
|
roleid = get_roleid(port->user_name);
|
|
|
|
foreach(line, parsed_hba_lines)
|
|
{
|
|
hba = (HbaLine *) lfirst(line);
|
|
|
|
/* Check connection type */
|
|
if (hba->conntype == ctLocal)
|
|
{
|
|
if (!IS_AF_UNIX(port->raddr.addr.ss_family))
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
if (IS_AF_UNIX(port->raddr.addr.ss_family))
|
|
continue;
|
|
|
|
/* Check SSL state */
|
|
#ifdef USE_SSL
|
|
if (port->ssl)
|
|
{
|
|
/* Connection is SSL, match both "host" and "hostssl" */
|
|
if (hba->conntype == ctHostNoSSL)
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
/* Connection is not SSL, match both "host" and "hostnossl" */
|
|
if (hba->conntype == ctHostSSL)
|
|
continue;
|
|
}
|
|
#else
|
|
/* No SSL support, so reject "hostssl" lines */
|
|
if (hba->conntype == ctHostSSL)
|
|
continue;
|
|
#endif
|
|
|
|
/* Check IP address */
|
|
switch (hba->ip_cmp_method)
|
|
{
|
|
case ipCmpMask:
|
|
if (!check_ip(&port->raddr,
|
|
(struct sockaddr *) & hba->addr,
|
|
(struct sockaddr *) & hba->mask))
|
|
continue;
|
|
break;
|
|
case ipCmpSameHost:
|
|
case ipCmpSameNet:
|
|
if (!check_same_host_or_net(&port->raddr,
|
|
hba->ip_cmp_method))
|
|
continue;
|
|
break;
|
|
default:
|
|
/* shouldn't get here, but deem it no-match if so */
|
|
continue;
|
|
}
|
|
} /* != ctLocal */
|
|
|
|
/* Check database and role */
|
|
if (!check_db(port->database_name, port->user_name, roleid,
|
|
hba->database))
|
|
continue;
|
|
|
|
if (!check_role(port->user_name, roleid, hba->role))
|
|
continue;
|
|
|
|
/* Found a record that matched! */
|
|
port->hba = hba;
|
|
return true;
|
|
}
|
|
|
|
/* If no matching entry was found, synthesize 'reject' entry. */
|
|
hba = palloc0(sizeof(HbaLine));
|
|
hba->auth_method = uaReject;
|
|
port->hba = hba;
|
|
return true;
|
|
|
|
/*
|
|
* XXX: Return false only happens if we have a parsing error, which we can
|
|
* no longer have (parsing now in postmaster). Consider changing API.
|
|
*/
|
|
}
|
|
|
|
/*
|
|
* Free an HbaLine structure
|
|
*/
|
|
static void
|
|
free_hba_record(HbaLine *record)
|
|
{
|
|
if (record->database)
|
|
pfree(record->database);
|
|
if (record->role)
|
|
pfree(record->role);
|
|
if (record->usermap)
|
|
pfree(record->usermap);
|
|
if (record->pamservice)
|
|
pfree(record->pamservice);
|
|
if (record->ldapserver)
|
|
pfree(record->ldapserver);
|
|
if (record->ldapprefix)
|
|
pfree(record->ldapprefix);
|
|
if (record->ldapsuffix)
|
|
pfree(record->ldapsuffix);
|
|
if (record->krb_server_hostname)
|
|
pfree(record->krb_server_hostname);
|
|
if (record->krb_realm)
|
|
pfree(record->krb_realm);
|
|
pfree(record);
|
|
}
|
|
|
|
/*
|
|
* Free all records on the parsed HBA list
|
|
*/
|
|
static void
|
|
clean_hba_list(List *lines)
|
|
{
|
|
ListCell *line;
|
|
|
|
foreach(line, lines)
|
|
{
|
|
HbaLine *parsed = (HbaLine *) lfirst(line);
|
|
|
|
if (parsed)
|
|
free_hba_record(parsed);
|
|
}
|
|
list_free(lines);
|
|
}
|
|
|
|
/*
|
|
* Read the config file and create a List of HbaLine records for the contents.
|
|
*
|
|
* The configuration is read into a temporary list, and if any parse error occurs
|
|
* the old list is kept in place and false is returned. Only if the whole file
|
|
* parses Ok is the list replaced, and the function returns true.
|
|
*/
|
|
bool
|
|
load_hba(void)
|
|
{
|
|
FILE *file;
|
|
List *hba_lines = NIL;
|
|
List *hba_line_nums = NIL;
|
|
ListCell *line,
|
|
*line_num;
|
|
List *new_parsed_lines = NIL;
|
|
bool ok = true;
|
|
|
|
file = AllocateFile(HbaFileName, "r");
|
|
if (file == NULL)
|
|
{
|
|
ereport(LOG,
|
|
(errcode_for_file_access(),
|
|
errmsg("could not open configuration file \"%s\": %m",
|
|
HbaFileName)));
|
|
|
|
/*
|
|
* Caller will take care of making this a FATAL error in case this is
|
|
* the initial startup. If it happens on reload, we just keep the old
|
|
* version around.
|
|
*/
|
|
return false;
|
|
}
|
|
|
|
tokenize_file(HbaFileName, file, &hba_lines, &hba_line_nums);
|
|
FreeFile(file);
|
|
|
|
/* Now parse all the lines */
|
|
forboth(line, hba_lines, line_num, hba_line_nums)
|
|
{
|
|
HbaLine *newline;
|
|
|
|
newline = palloc0(sizeof(HbaLine));
|
|
|
|
if (!parse_hba_line(lfirst(line), lfirst_int(line_num), newline))
|
|
{
|
|
/* Parse error in the file, so indicate there's a problem */
|
|
free_hba_record(newline);
|
|
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 the temporary lists */
|
|
free_lines(&hba_lines, &hba_line_nums);
|
|
|
|
if (!ok)
|
|
{
|
|
/* Parsing failed at one or more rows, so bail out */
|
|
clean_hba_list(new_parsed_lines);
|
|
return false;
|
|
}
|
|
|
|
/* Loaded new file successfully, replace the one we use */
|
|
clean_hba_list(parsed_hba_lines);
|
|
parsed_hba_lines = new_parsed_lines;
|
|
|
|
return true;
|
|
}
|
|
|
|
/*
|
|
* Process one line from the ident config file.
|
|
*
|
|
* 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.
|
|
*/
|
|
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)
|
|
{
|
|
ListCell *line_item;
|
|
char *token;
|
|
char *file_map;
|
|
char *file_pgrole;
|
|
char *file_ident_user;
|
|
|
|
*found_p = false;
|
|
*error_p = false;
|
|
|
|
Assert(line != NIL);
|
|
line_item = list_head(line);
|
|
|
|
/* Get the map token (must exist) */
|
|
token = lfirst(line_item);
|
|
file_map = token;
|
|
|
|
/* Get the ident user token */
|
|
line_item = lnext(line_item);
|
|
if (!line_item)
|
|
goto ident_syntax;
|
|
token = lfirst(line_item);
|
|
file_ident_user = token;
|
|
|
|
/* Get the PG rolename token */
|
|
line_item = lnext(line_item);
|
|
if (!line_item)
|
|
goto ident_syntax;
|
|
token = lfirst(line_item);
|
|
file_pgrole = token;
|
|
|
|
if (strcmp(file_map, usermap_name) != 0)
|
|
/* Line does not match the map name we're looking for, so just abort */
|
|
return;
|
|
|
|
/* Match? */
|
|
if (file_ident_user[0] == '/')
|
|
{
|
|
/*
|
|
* When system username starts with a slash, treat it as a regular
|
|
* expression. In this case, we process the system username as a
|
|
* regular expression that returns exactly one match. This is replaced
|
|
* 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);
|
|
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);
|
|
if (r)
|
|
{
|
|
char errstr[100];
|
|
|
|
if (r != REG_NOMATCH)
|
|
{
|
|
/* REG_NOMATCH is not an error, everything else is */
|
|
pg_regerror(r, &re, errstr, sizeof(errstr));
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_INVALID_REGULAR_EXPRESSION),
|
|
errmsg("regular expression match for \"%s\" failed: %s", file_ident_user + 1, errstr)));
|
|
*error_p = true;
|
|
}
|
|
|
|
pfree(wstr);
|
|
pg_regfree(&re);
|
|
return;
|
|
}
|
|
pfree(wstr);
|
|
|
|
if ((ofs = strstr(file_pgrole, "\\1")) != NULL)
|
|
{
|
|
/* substitution of the first argument requested */
|
|
if (matches[1].rm_so < 0)
|
|
{
|
|
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);
|
|
*error_p = true;
|
|
return;
|
|
}
|
|
|
|
/*
|
|
* 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));
|
|
memcpy(regexp_pgrole + strlen(regexp_pgrole),
|
|
ident_user + matches[1].rm_so,
|
|
matches[1].rm_eo - matches[1].rm_so);
|
|
strcat(regexp_pgrole, ofs + 2);
|
|
}
|
|
else
|
|
{
|
|
/* no substitution, so copy the match */
|
|
regexp_pgrole = pstrdup(file_pgrole);
|
|
}
|
|
|
|
pg_regfree(&re);
|
|
|
|
/*
|
|
* now check if the username actually matched what the user is trying
|
|
* to connect as
|
|
*/
|
|
if (case_insensitive)
|
|
{
|
|
if (pg_strcasecmp(regexp_pgrole, pg_role) == 0)
|
|
*found_p = true;
|
|
}
|
|
else
|
|
{
|
|
if (strcmp(regexp_pgrole, pg_role) == 0)
|
|
*found_p = true;
|
|
}
|
|
pfree(regexp_pgrole);
|
|
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
/* 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)
|
|
*found_p = true;
|
|
}
|
|
else
|
|
{
|
|
if (strcmp(file_pgrole, pg_role) == 0 &&
|
|
strcmp(file_ident_user, ident_user) == 0)
|
|
*found_p = true;
|
|
}
|
|
}
|
|
|
|
return;
|
|
|
|
ident_syntax:
|
|
ereport(LOG,
|
|
(errcode(ERRCODE_CONFIG_FILE_ERROR),
|
|
errmsg("missing entry in file \"%s\" at end of line %d",
|
|
IdentFileName, line_number)));
|
|
*error_p = true;
|
|
}
|
|
|
|
|
|
/*
|
|
* Scan the (pre-parsed) ident usermap file line by line, looking for a match
|
|
*
|
|
* See if the user with ident username "ident_user" is allowed to act
|
|
* as Postgres user "pgrole" according to usermap "usermap_name".
|
|
*
|
|
* Special case: Usermap NULL, equivalent to what was previously called
|
|
* "sameuser" or "samerole", means don't look in the usermap
|
|
* file. That's an implied map where "pgrole" must be identical to
|
|
* "ident_user" in order to be authorized.
|
|
*
|
|
* Iff authorized, return STATUS_OK, otherwise return STATUS_ERROR.
|
|
*/
|
|
int
|
|
check_usermap(const char *usermap_name,
|
|
const char *pg_role,
|
|
const char *auth_user,
|
|
bool case_insensitive)
|
|
{
|
|
bool found_entry = false,
|
|
error = false;
|
|
|
|
if (usermap_name == NULL || usermap_name[0] == '\0')
|
|
{
|
|
if (case_insensitive)
|
|
{
|
|
if (pg_strcasecmp(pg_role, auth_user) == 0)
|
|
return STATUS_OK;
|
|
}
|
|
else
|
|
{
|
|
if (strcmp(pg_role, auth_user) == 0)
|
|
return STATUS_OK;
|
|
}
|
|
ereport(LOG,
|
|
(errmsg("provided user name (%s) and authenticated user name (%s) do not match",
|
|
auth_user, pg_role)));
|
|
return STATUS_ERROR;
|
|
}
|
|
else
|
|
{
|
|
ListCell *line_cell,
|
|
*num_cell;
|
|
|
|
forboth(line_cell, ident_lines, num_cell, ident_line_nums)
|
|
{
|
|
parse_ident_usermap(lfirst(line_cell), lfirst_int(num_cell),
|
|
usermap_name, pg_role, auth_user, case_insensitive,
|
|
&found_entry, &error);
|
|
if (found_entry || error)
|
|
break;
|
|
}
|
|
}
|
|
if (!found_entry && !error)
|
|
{
|
|
ereport(LOG,
|
|
(errmsg("no match in usermap for user \"%s\" authenticated as \"%s\"",
|
|
pg_role, auth_user),
|
|
errcontext("usermap \"%s\"", usermap_name)));
|
|
}
|
|
return found_entry ? STATUS_OK : STATUS_ERROR;
|
|
}
|
|
|
|
|
|
/*
|
|
* Read the ident config file and create a List of Lists of tokens in the file.
|
|
*/
|
|
void
|
|
load_ident(void)
|
|
{
|
|
FILE *file;
|
|
|
|
if (ident_lines || ident_line_nums)
|
|
free_lines(&ident_lines, &ident_line_nums);
|
|
|
|
file = AllocateFile(IdentFileName, "r");
|
|
if (file == NULL)
|
|
{
|
|
/* not fatal ... we just won't do any special ident maps */
|
|
ereport(LOG,
|
|
(errcode_for_file_access(),
|
|
errmsg("could not open Ident usermap file \"%s\": %m",
|
|
IdentFileName)));
|
|
}
|
|
else
|
|
{
|
|
tokenize_file(IdentFileName, file, &ident_lines, &ident_line_nums);
|
|
FreeFile(file);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
* Determine what authentication method should be used when accessing database
|
|
* "database" from frontend "raddr", user "user". Return the method and
|
|
* an optional argument (stored in fields of *port), and STATUS_OK.
|
|
*
|
|
* Note that STATUS_ERROR indicates a problem with the hba config file.
|
|
* If the file is OK but does not contain any entry matching the request,
|
|
* we return STATUS_OK and method = uaReject.
|
|
*/
|
|
int
|
|
hba_getauthmethod(hbaPort *port)
|
|
{
|
|
if (check_hba(port))
|
|
return STATUS_OK;
|
|
else
|
|
return STATUS_ERROR;
|
|
}
|