diff options
Diffstat (limited to '')
-rw-r--r-- | src/backend/libpq/hba.c | 3166 |
1 files changed, 3166 insertions, 0 deletions
diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c new file mode 100644 index 0000000..64e59d4 --- /dev/null +++ b/src/backend/libpq/hba.c @@ -0,0 +1,3166 @@ +/*------------------------------------------------------------------------- + * + * 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-2021, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/libpq/hba.c + * + *------------------------------------------------------------------------- + */ +#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 "access/htup_details.h" +#include "catalog/pg_collation.h" +#include "catalog/pg_type.h" +#include "common/ip.h" +#include "common/string.h" +#include "funcapi.h" +#include "libpq/ifaddr.h" +#include "libpq/libpq.h" +#include "miscadmin.h" +#include "postmaster/postmaster.h" +#include "regex/regex.h" +#include "replication/walsender.h" +#include "storage/fd.h" +#include "utils/acl.h" +#include "utils/builtins.h" +#include "utils/guc.h" +#include "utils/lsyscache.h" +#include "utils/memutils.h" +#include "utils/varlena.h" + +#ifdef USE_LDAP +#ifdef WIN32 +#include <winldap.h> +#else +#include <ldap.h> +#endif +#endif + + +#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; + + +#define token_is_keyword(t, k) (!t->quoted && strcmp(t->string, k) == 0) +#define token_matches(t, k) (strcmp(t->string, k) == 0) + +/* + * A single string token lexed from a config file, together with whether + * the token had been quoted. + */ +typedef struct HbaToken +{ + char *string; + bool quoted; +} HbaToken; + +/* + * TokenizedLine represents one line lexed from a config file. + * Each item in the "fields" list is a sub-list of HbaTokens. + * We don't emit a TokenizedLine for empty or all-comment lines, + * so "fields" is never NIL (nor are any of its sub-lists). + * Exception: if an error occurs during tokenization, we might + * have fields == NIL, in which case err_msg != NULL. + */ +typedef struct TokenizedLine +{ + List *fields; /* List of lists of HbaTokens */ + int line_num; /* Line number */ + char *raw_line; /* Raw line text */ + char *err_msg; /* Error message if any */ +} TokenizedLine; + +/* + * pre-parsed content of HBA config file: list of HbaLine structs. + * parsed_hba_context is the memory context where it lives. + */ +static List *parsed_hba_lines = NIL; +static MemoryContext parsed_hba_context = NULL; + +/* + * 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 explicitly free'd. + */ +static List *parsed_ident_lines = NIL; +static MemoryContext parsed_ident_context = NULL; + +/* + * The following character array represents the names of the authentication + * methods that are supported by PostgreSQL. + * + * Note: keep this in sync with the UserAuth enum in hba.h. + */ +static const char *const UserAuthName[] = +{ + "reject", + "implicit reject", /* Not a user-visible option */ + "trust", + "ident", + "password", + "md5", + "scram-sha-256", + "gss", + "sspi", + "pam", + "bsd", + "ldap", + "cert", + "radius", + "peer" +}; + + +static MemoryContext tokenize_file(const char *filename, FILE *file, + List **tok_lines, int elevel); +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 ArrayType *gethba_options(HbaLine *hba); +static void fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, + int lineno, HbaLine *hba, const char *err_msg); +static void fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc); + + +/* + * 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 the string pointed to by *lineptr. + * + * 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 or '#', but not newlines). As in SQL, write two double-quotes + * to represent a double quote. + * + * Comments (started by an unquoted '#') are skipped, i.e. the remainder + * of the line is ignored. + * + * (Note that line continuation processing happens before tokenization. + * Thus, if a continuation occurs within quoted text or a comment, the + * quoted text or comment is considered to continue to the next line.) + * + * The token, if any, is returned at *buf (a buffer of size bufsz), and + * *lineptr is advanced past the token. + * + * 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.) + * + * We set *terminating_comma to indicate whether the token is terminated by a + * comma (which is not returned). + * + * In event of an error, log a message at ereport level elevel, and also + * set *err_msg to a string describing the error. Currently the only + * possible error is token too long for buf. + * + * If successful: store null-terminated token at *buf and return true. + * If no more tokens on line: set *buf = '\0' and return false. + * If error: fill buf with truncated or misformatted token and return false. + */ +static bool +next_token(char **lineptr, char *buf, int bufsz, + bool *initial_quote, bool *terminating_comma, + int elevel, char **err_msg) +{ + int c; + char *start_buf = buf; + char *end_buf = buf + (bufsz - 1); + bool in_quote = false; + bool was_quote = false; + bool saw_quote = false; + + Assert(end_buf > start_buf); + + *initial_quote = false; + *terminating_comma = false; + + /* Move over any whitespace and commas preceding the next token */ + while ((c = (*(*lineptr)++)) != '\0' && (pg_isblank(c) || c == ',')) + ; + + /* + * Build a token in buf of next characters up to EOL, unquoted comma, or + * unquoted whitespace. + */ + while (c != '\0' && + (!pg_isblank(c) || in_quote)) + { + /* skip comments to EOL */ + if (c == '#' && !in_quote) + { + while ((c = (*(*lineptr)++)) != '\0') + ; + break; + } + + if (buf >= end_buf) + { + *buf = '\0'; + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("authentication file token too long, skipping: \"%s\"", + start_buf))); + *err_msg = "authentication file token too long"; + /* Discard remainder of line */ + while ((c = (*(*lineptr)++)) != '\0') + ; + /* Un-eat the '\0', in case we're called again */ + (*lineptr)--; + return false; + } + + /* we do not pass back a terminating comma in the token */ + if (c == ',' && !in_quote) + { + *terminating_comma = true; + break; + } + + if (c != '"' || was_quote) + *buf++ = c; + + /* 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 = *(*lineptr)++; + } + + /* + * Un-eat the char right after the token (critical in case it is '\0', + * else next call will read past end of string). + */ + (*lineptr)--; + + *buf = '\0'; + + return (saw_quote || buf > start_buf); +} + +/* + * Construct a palloc'd HbaToken struct, copying the given string. + */ +static HbaToken * +make_hba_token(const char *token, bool quoted) +{ + HbaToken *hbatoken; + int toklen; + + toklen = strlen(token); + /* we copy string into same palloc block as the struct */ + hbatoken = (HbaToken *) palloc(sizeof(HbaToken) + toklen + 1); + hbatoken->string = (char *) hbatoken + sizeof(HbaToken); + hbatoken->quoted = quoted; + memcpy(hbatoken->string, token, toklen + 1); + + return hbatoken; +} + +/* + * Copy a HbaToken struct into freshly palloc'd memory. + */ +static HbaToken * +copy_hba_token(HbaToken *in) +{ + HbaToken *out = make_hba_token(in->string, in->quoted); + + return out; +} + + +/* + * Tokenize one HBA field from a line, handling file inclusion and comma lists. + * + * filename: current file's pathname (needed to resolve relative pathnames) + * *lineptr: current line pointer, which will be advanced past field + * + * In event of an error, log a message at ereport level elevel, and also + * set *err_msg to a string describing the error. Note that the result + * may be non-NIL anyway, so *err_msg must be tested to determine whether + * there was an error. + * + * The result is a List of HbaToken structs, one for each token in the field, + * or NIL if we reached EOL. + */ +static List * +next_field_expand(const char *filename, char **lineptr, + int elevel, char **err_msg) +{ + char buf[MAX_TOKEN]; + bool trailing_comma; + bool initial_quote; + List *tokens = NIL; + + do + { + if (!next_token(lineptr, buf, sizeof(buf), + &initial_quote, &trailing_comma, + elevel, err_msg)) + break; + + /* Is this referencing a file? */ + if (!initial_quote && buf[0] == '@' && buf[1] != '\0') + tokens = tokenize_inc_file(tokens, filename, buf + 1, + elevel, err_msg); + else + tokens = lappend(tokens, make_hba_token(buf, initial_quote)); + } while (trailing_comma && (*err_msg == NULL)); + + return tokens; +} + +/* + * tokenize_inc_file + * Expand a file included from another file into an hba "field" + * + * Opens and tokenises a file included from another HBA config file with @, + * and returns all values found therein as a flat list of HbaTokens. If a + * @-token is found, recursively expand it. The newly read tokens are + * appended to "tokens" (so that foo,bar,@baz does what you expect). + * All new tokens are allocated in caller's memory context. + * + * In event of an error, log a message at ereport level elevel, and also + * set *err_msg to a string describing the error. Note that the result + * may be non-NIL anyway, so *err_msg must be tested to determine whether + * there was an error. + */ +static List * +tokenize_inc_file(List *tokens, + const char *outer_filename, + const char *inc_filename, + int elevel, + char **err_msg) +{ + char *inc_fullname; + FILE *inc_file; + List *inc_lines; + ListCell *inc_line; + MemoryContext linecxt; + + 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) + { + int save_errno = errno; + + ereport(elevel, + (errcode_for_file_access(), + errmsg("could not open secondary authentication file \"@%s\" as \"%s\": %m", + inc_filename, inc_fullname))); + *err_msg = psprintf("could not open secondary authentication file \"@%s\" as \"%s\": %s", + inc_filename, inc_fullname, strerror(save_errno)); + pfree(inc_fullname); + return tokens; + } + + /* There is possible recursion here if the file contains @ */ + linecxt = tokenize_file(inc_fullname, inc_file, &inc_lines, elevel); + + FreeFile(inc_file); + pfree(inc_fullname); + + /* Copy all tokens found in the file and append to the tokens list */ + foreach(inc_line, inc_lines) + { + TokenizedLine *tok_line = (TokenizedLine *) lfirst(inc_line); + ListCell *inc_field; + + /* If any line has an error, propagate that up to caller */ + if (tok_line->err_msg) + { + *err_msg = pstrdup(tok_line->err_msg); + break; + } + + foreach(inc_field, tok_line->fields) + { + List *inc_tokens = lfirst(inc_field); + ListCell *inc_token; + + foreach(inc_token, inc_tokens) + { + HbaToken *token = lfirst(inc_token); + + tokens = lappend(tokens, copy_hba_token(token)); + } + } + } + + MemoryContextDelete(linecxt); + return tokens; +} + +/* + * Tokenize the given file. + * + * The output is a list of TokenizedLine structs; see struct definition above. + * + * filename: the absolute path to the target file + * file: the already-opened target file + * tok_lines: receives output list + * elevel: message logging level + * + * Errors are reported by logging messages at ereport level elevel and by + * adding TokenizedLine structs containing non-null err_msg fields to the + * output list. + * + * Return value is a memory context which contains all memory allocated by + * this function (it's a child of caller's context). + */ +static MemoryContext +tokenize_file(const char *filename, FILE *file, List **tok_lines, int elevel) +{ + int line_number = 1; + StringInfoData buf; + MemoryContext linecxt; + MemoryContext oldcxt; + + linecxt = AllocSetContextCreate(CurrentMemoryContext, + "tokenize_file", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(linecxt); + + initStringInfo(&buf); + + *tok_lines = NIL; + + while (!feof(file) && !ferror(file)) + { + char *lineptr; + List *current_line = NIL; + char *err_msg = NULL; + int last_backslash_buflen = 0; + int continuations = 0; + + /* Collect the next input line, handling backslash continuations */ + resetStringInfo(&buf); + + while (pg_get_line_append(file, &buf)) + { + /* Strip trailing newline, including \r in case we're on Windows */ + buf.len = pg_strip_crlf(buf.data); + + /* + * Check for backslash continuation. The backslash must be after + * the last place we found a continuation, else two backslashes + * followed by two \n's would behave surprisingly. + */ + if (buf.len > last_backslash_buflen && + buf.data[buf.len - 1] == '\\') + { + /* Continuation, so strip it and keep reading */ + buf.data[--buf.len] = '\0'; + last_backslash_buflen = buf.len; + continuations++; + continue; + } + + /* Nope, so we have the whole line */ + break; + } + + if (ferror(file)) + { + /* I/O error! */ + int save_errno = errno; + + ereport(elevel, + (errcode_for_file_access(), + errmsg("could not read file \"%s\": %m", filename))); + err_msg = psprintf("could not read file \"%s\": %s", + filename, strerror(save_errno)); + break; + } + + /* Parse fields */ + lineptr = buf.data; + while (*lineptr && err_msg == NULL) + { + List *current_field; + + current_field = next_field_expand(filename, &lineptr, + elevel, &err_msg); + /* add field to line, unless we are at EOL or comment start */ + if (current_field != NIL) + current_line = lappend(current_line, current_field); + } + + /* Reached EOL; emit line to TokenizedLine list unless it's boring */ + if (current_line != NIL || err_msg != NULL) + { + TokenizedLine *tok_line; + + tok_line = (TokenizedLine *) palloc(sizeof(TokenizedLine)); + tok_line->fields = current_line; + tok_line->line_num = line_number; + tok_line->raw_line = pstrdup(buf.data); + tok_line->err_msg = err_msg; + *tok_lines = lappend(*tok_lines, tok_line); + } + + line_number += continuations + 1; + } + + MemoryContextSwitchTo(oldcxt); + + return linecxt; +} + + +/* + * 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_role_oid(role, true); + + if (!OidIsValid(roleid)) + return false; /* if target role not exist, say "no" */ + + /* + * See if user is directly or indirectly a member of role. For this + * purpose, a superuser is not considered to be automatically a member of + * the role, so group auth only applies to explicit membership. + */ + return is_member_of_role_nosuper(userid, roleid); +} + +/* + * Check HbaToken list for a match to role, allowing group names. + */ +static bool +check_role(const char *role, Oid roleid, List *tokens) +{ + ListCell *cell; + HbaToken *tok; + + foreach(cell, tokens) + { + tok = lfirst(cell); + 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; + } + return false; +} + +/* + * Check to see if db/role combination matches HbaToken list. + */ +static bool +check_db(const char *dbname, const char *role, Oid roleid, List *tokens) +{ + ListCell *cell; + HbaToken *tok; + + foreach(cell, tokens) + { + tok = lfirst(cell); + if (am_walsender && !am_db_walsender) + { + /* + * physical replication walsender connections can only match + * replication keyword + */ + if (token_is_keyword(tok, "replication")) + return true; + } + else if (token_is_keyword(tok, "all")) + return true; + 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_is_keyword(tok, "replication")) + continue; /* never match this if not walsender */ + else if (token_matches(tok, dbname)) + return true; + } + return false; +} + +static bool +ipv4eq(struct sockaddr_in *a, struct sockaddr_in *b) +{ + return (a->sin_addr.s_addr == b->sin_addr.s_addr); +} + +#ifdef HAVE_IPV6 + +static bool +ipv6eq(struct sockaddr_in6 *a, struct sockaddr_in6 *b) +{ + int i; + + for (i = 0; i < 16; i++) + if (a->sin6_addr.s6_addr[i] != b->sin6_addr.s6_addr[i]) + return false; + + return true; +} +#endif /* HAVE_IPV6 */ + +/* + * Check whether host name matches pattern. + */ +static bool +hostname_match(const char *pattern, const char *actual_hostname) +{ + if (pattern[0] == '.') /* suffix match */ + { + size_t plen = strlen(pattern); + size_t hlen = strlen(actual_hostname); + + if (hlen < plen) + return false; + + return (pg_strcasecmp(pattern, actual_hostname + (hlen - plen)) == 0); + } + else + return (pg_strcasecmp(pattern, actual_hostname) == 0); +} + +/* + * Check to see if a connecting IP matches a given host name. + */ +static bool +check_hostname(hbaPort *port, const char *hostname) +{ + struct addrinfo *gai_result, + *gai; + int ret; + bool found; + + /* Quick out if remote host name already known bad */ + if (port->remote_hostname_resolv < 0) + return false; + + /* Lookup remote host name if not already done */ + if (!port->remote_hostname) + { + char remote_hostname[NI_MAXHOST]; + + ret = pg_getnameinfo_all(&port->raddr.addr, port->raddr.salen, + remote_hostname, sizeof(remote_hostname), + NULL, 0, + NI_NAMEREQD); + if (ret != 0) + { + /* remember failure; don't complain in the postmaster log yet */ + port->remote_hostname_resolv = -2; + port->remote_hostname_errcode = ret; + return false; + } + + port->remote_hostname = pstrdup(remote_hostname); + } + + /* Now see if remote host name matches this pg_hba line */ + if (!hostname_match(hostname, port->remote_hostname)) + return false; + + /* If we already verified the forward lookup, we're done */ + if (port->remote_hostname_resolv == +1) + return true; + + /* Lookup IP from host name and check against original IP */ + ret = getaddrinfo(port->remote_hostname, NULL, NULL, &gai_result); + if (ret != 0) + { + /* remember failure; don't complain in the postmaster log yet */ + port->remote_hostname_resolv = -2; + port->remote_hostname_errcode = ret; + return false; + } + + found = false; + for (gai = gai_result; gai; gai = gai->ai_next) + { + if (gai->ai_addr->sa_family == port->raddr.addr.ss_family) + { + if (gai->ai_addr->sa_family == AF_INET) + { + if (ipv4eq((struct sockaddr_in *) gai->ai_addr, + (struct sockaddr_in *) &port->raddr.addr)) + { + found = true; + break; + } + } +#ifdef HAVE_IPV6 + else if (gai->ai_addr->sa_family == AF_INET6) + { + if (ipv6eq((struct sockaddr_in6 *) gai->ai_addr, + (struct sockaddr_in6 *) &port->raddr.addr)) + { + found = true; + break; + } + } +#endif + } + } + + if (gai_result) + freeaddrinfo(gai_result); + + if (!found) + elog(DEBUG2, "pg_hba.conf host name \"%s\" rejected because address resolution did not return a match with IP address of client", + hostname); + + port->remote_hostname_resolv = found ? +1 : -1; + + return found; +} + +/* + * 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 && + pg_range_sockaddr(&raddr->addr, + (struct sockaddr_storage *) addr, + (struct sockaddr_storage *) mask)) + return true; + return false; +} + +/* + * 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) + { + ereport(LOG, + (errmsg("error enumerating network interfaces: %m"))); + return false; + } + + return cn.result; +} + + +/* + * Macros used to check and report on invalid configuration options. + * On error: log a message at level elevel, set *err_msg, and exit the function. + * These macros are not as general-purpose as they look, because they know + * what the calling function's error-exit value is. + * + * 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(elevel, \ + (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))); \ + *err_msg = psprintf("authentication option \"%s\" is only valid for authentication methods %s", \ + optname, validmethods); \ + return false; \ +} while (0) + +#define REQUIRE_AUTH_OPTION(methodval, optname, validmethods) \ +do { \ + if (hbaline->auth_method != methodval) \ + INVALID_AUTH_OPTION(optname, validmethods); \ +} while (0) + +#define MANDATORY_AUTH_ARG(argvar, argname, authname) \ +do { \ + if (argvar == NULL) { \ + ereport(elevel, \ + (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))); \ + *err_msg = psprintf("authentication method \"%s\" requires argument \"%s\" to be set", \ + authname, argname); \ + return NULL; \ + } \ +} while (0) + +/* + * Macros for handling pg_ident problems. + * Much as above, but currently the message level is hardwired as LOG + * and there is no provision for an err_msg string. + * + * IDENT_FIELD_ABSENT: + * Log a message and exit the function if the given ident field ListCell is + * not populated. + * + * IDENT_MULTI_VALUE: + * Log a message and exit the function if the given ident token List has more + * than one element. + */ +#define IDENT_FIELD_ABSENT(field) \ +do { \ + if (!field) { \ + ereport(LOG, \ + (errcode(ERRCODE_CONFIG_FILE_ERROR), \ + errmsg("missing entry in file \"%s\" at end of line %d", \ + IdentFileName, line_num))); \ + return NULL; \ + } \ +} while (0) + +#define IDENT_MULTI_VALUE(tokens) \ +do { \ + if (tokens->length > 1) { \ + ereport(LOG, \ + (errcode(ERRCODE_CONFIG_FILE_ERROR), \ + errmsg("multiple values in ident field"), \ + errcontext("line %d of configuration file \"%s\"", \ + line_num, IdentFileName))); \ + return NULL; \ + } \ +} while (0) + + +/* + * Parse one tokenised line from the hba config file and store the result in a + * HbaLine structure. + * + * If parsing fails, log a message at ereport level elevel, store an error + * string in tok_line->err_msg, and return NULL. (Some non-error conditions + * can also result in such messages.) + * + * 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 HbaLine * +parse_hba_line(TokenizedLine *tok_line, int elevel) +{ + int line_num = tok_line->line_num; + char **err_msg = &tok_line->err_msg; + char *str; + struct addrinfo *gai_result; + struct addrinfo hints; + int ret; + char *cidr_slash; + char *unsupauth; + ListCell *field; + List *tokens; + ListCell *tokencell; + HbaToken *token; + HbaLine *parsedline; + + parsedline = palloc0(sizeof(HbaLine)); + parsedline->linenumber = line_num; + parsedline->rawline = pstrdup(tok_line->raw_line); + + /* Check the record type. */ + Assert(tok_line->fields != NIL); + field = list_head(tok_line->fields); + tokens = lfirst(field); + if (tokens->length > 1) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple values specified for connection type"), + errhint("Specify exactly one connection type per line."), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "multiple values specified for connection type"; + return NULL; + } + token = linitial(tokens); + if (strcmp(token->string, "local") == 0) + { +#ifdef HAVE_UNIX_SOCKETS + parsedline->conntype = ctLocal; +#else + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("local connections are not supported by this build"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "local connections are not supported by this build"; + return NULL; +#endif + } + else if (strcmp(token->string, "host") == 0 || + strcmp(token->string, "hostssl") == 0 || + strcmp(token->string, "hostnossl") == 0 || + strcmp(token->string, "hostgssenc") == 0 || + strcmp(token->string, "hostnogssenc") == 0) + { + + if (token->string[4] == 's') /* "hostssl" */ + { + parsedline->conntype = ctHostSSL; + /* Log a warning if SSL support is not active */ +#ifdef USE_SSL + if (!EnableSSL) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("hostssl record cannot match because SSL is disabled"), + errhint("Set ssl = on in postgresql.conf."), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "hostssl record cannot match because SSL is disabled"; + } +#else + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("hostssl record cannot match because SSL is not supported by this build"), + errhint("Compile with --with-ssl to use SSL connections."), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "hostssl record cannot match because SSL is not supported by this build"; +#endif + } + else if (token->string[4] == 'g') /* "hostgssenc" */ + { + parsedline->conntype = ctHostGSS; +#ifndef ENABLE_GSS + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("hostgssenc record cannot match because GSSAPI is not supported by this build"), + errhint("Compile with --with-gssapi to use GSSAPI connections."), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build"; +#endif + } + else if (token->string[4] == 'n' && token->string[6] == 's') + parsedline->conntype = ctHostNoSSL; + else if (token->string[4] == 'n' && token->string[6] == 'g') + parsedline->conntype = ctHostNoGSS; + else + { + /* "host" */ + parsedline->conntype = ctHost; + } + } /* record type */ + else + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid connection type \"%s\"", + token->string), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("invalid connection type \"%s\"", token->string); + return NULL; + } + + /* Get the databases. */ + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("end-of-line before database specification"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "end-of-line before database specification"; + return NULL; + } + parsedline->databases = NIL; + tokens = lfirst(field); + foreach(tokencell, tokens) + { + parsedline->databases = lappend(parsedline->databases, + copy_hba_token(lfirst(tokencell))); + } + + /* Get the roles. */ + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("end-of-line before role specification"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "end-of-line before role specification"; + return NULL; + } + parsedline->roles = NIL; + tokens = lfirst(field); + foreach(tokencell, tokens) + { + parsedline->roles = lappend(parsedline->roles, + copy_hba_token(lfirst(tokencell))); + } + + if (parsedline->conntype != ctLocal) + { + /* Read the IP address field. (with or without CIDR netmask) */ + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("end-of-line before IP address specification"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "end-of-line before IP address specification"; + return NULL; + } + tokens = lfirst(field); + if (tokens->length > 1) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple values specified for host address"), + errhint("Specify one address range per line."), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "multiple values specified for host address"; + return NULL; + } + token = linitial(tokens); + + if (token_is_keyword(token, "all")) + { + parsedline->ip_cmp_method = ipCmpAll; + } + else if (token_is_keyword(token, "samehost")) + { + /* Any IP on this host is allowed to connect */ + parsedline->ip_cmp_method = ipCmpSameHost; + } + else if (token_is_keyword(token, "samenet")) + { + /* 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 */ + 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'; + + /* Get the IP address either way */ + hints.ai_flags = AI_NUMERICHOST; + hints.ai_family = AF_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(str, NULL, &hints, &gai_result); + if (ret == 0 && gai_result) + { + memcpy(&parsedline->addr, gai_result->ai_addr, + gai_result->ai_addrlen); + parsedline->addrlen = gai_result->ai_addrlen; + } + else if (ret == EAI_NONAME) + parsedline->hostname = str; + else + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid IP address \"%s\": %s", + str, gai_strerror(ret)), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("invalid IP address \"%s\": %s", + str, gai_strerror(ret)); + if (gai_result) + pg_freeaddrinfo_all(hints.ai_family, gai_result); + return NULL; + } + + pg_freeaddrinfo_all(hints.ai_family, gai_result); + + /* Get the netmask */ + if (cidr_slash) + { + if (parsedline->hostname) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("specifying both host name and CIDR mask is invalid: \"%s\"", + token->string), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("specifying both host name and CIDR mask is invalid: \"%s\"", + token->string); + return NULL; + } + + if (pg_sockaddr_cidr_mask(&parsedline->mask, cidr_slash + 1, + parsedline->addr.ss_family) < 0) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid CIDR mask in address \"%s\"", + token->string), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("invalid CIDR mask in address \"%s\"", + token->string); + return NULL; + } + parsedline->masklen = parsedline->addrlen; + pfree(str); + } + else if (!parsedline->hostname) + { + /* Read the mask field. */ + pfree(str); + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("end-of-line before netmask specification"), + errhint("Specify an address range in CIDR notation, or provide a separate netmask."), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "end-of-line before netmask specification"; + return NULL; + } + tokens = lfirst(field); + if (tokens->length > 1) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple values specified for netmask"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "multiple values specified for netmask"; + return NULL; + } + token = linitial(tokens); + + ret = pg_getaddrinfo_all(token->string, NULL, + &hints, &gai_result); + if (ret || !gai_result) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid IP mask \"%s\": %s", + token->string, gai_strerror(ret)), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("invalid IP mask \"%s\": %s", + token->string, gai_strerror(ret)); + if (gai_result) + pg_freeaddrinfo_all(hints.ai_family, gai_result); + return NULL; + } + + memcpy(&parsedline->mask, gai_result->ai_addr, + gai_result->ai_addrlen); + parsedline->masklen = gai_result->ai_addrlen; + pg_freeaddrinfo_all(hints.ai_family, gai_result); + + if (parsedline->addr.ss_family != parsedline->mask.ss_family) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("IP address and mask do not match"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "IP address and mask do not match"; + return NULL; + } + } + } + } /* != ctLocal */ + + /* Get the authentication method */ + field = lnext(tok_line->fields, field); + if (!field) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("end-of-line before authentication method"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "end-of-line before authentication method"; + return NULL; + } + tokens = lfirst(field); + if (tokens->length > 1) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple values specified for authentication type"), + errhint("Specify exactly one authentication type per line."), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "multiple values specified for authentication type"; + return NULL; + } + token = linitial(tokens); + + unsupauth = NULL; + if (strcmp(token->string, "trust") == 0) + parsedline->auth_method = uaTrust; + else if (strcmp(token->string, "ident") == 0) + parsedline->auth_method = uaIdent; + else if (strcmp(token->string, "peer") == 0) + parsedline->auth_method = uaPeer; + else if (strcmp(token->string, "password") == 0) + parsedline->auth_method = uaPassword; + else if (strcmp(token->string, "gss") == 0) +#ifdef ENABLE_GSS + parsedline->auth_method = uaGSS; +#else + unsupauth = "gss"; +#endif + else if (strcmp(token->string, "sspi") == 0) +#ifdef ENABLE_SSPI + parsedline->auth_method = uaSSPI; +#else + unsupauth = "sspi"; +#endif + else if (strcmp(token->string, "reject") == 0) + parsedline->auth_method = uaReject; + else if (strcmp(token->string, "md5") == 0) + { + if (Db_user_namespace) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "MD5 authentication is not supported when \"db_user_namespace\" is enabled"; + return NULL; + } + parsedline->auth_method = uaMD5; + } + else if (strcmp(token->string, "scram-sha-256") == 0) + parsedline->auth_method = uaSCRAM; + else if (strcmp(token->string, "pam") == 0) +#ifdef USE_PAM + parsedline->auth_method = uaPAM; +#else + unsupauth = "pam"; +#endif + else if (strcmp(token->string, "bsd") == 0) +#ifdef USE_BSD_AUTH + parsedline->auth_method = uaBSD; +#else + unsupauth = "bsd"; +#endif + else if (strcmp(token->string, "ldap") == 0) +#ifdef USE_LDAP + parsedline->auth_method = uaLDAP; +#else + unsupauth = "ldap"; +#endif + else if (strcmp(token->string, "cert") == 0) +#ifdef USE_SSL + parsedline->auth_method = uaCert; +#else + unsupauth = "cert"; +#endif + else if (strcmp(token->string, "radius") == 0) + parsedline->auth_method = uaRADIUS; + else + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid authentication method \"%s\"", + token->string), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("invalid authentication method \"%s\"", + token->string); + return NULL; + } + + if (unsupauth) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid authentication method \"%s\": not supported by this build", + token->string), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("invalid authentication method \"%s\": not supported by this build", + token->string); + return NULL; + } + + /* + * XXX: When using ident on local connections, change it to peer, for + * backwards compatibility. + */ + if (parsedline->conntype == ctLocal && + parsedline->auth_method == uaIdent) + parsedline->auth_method = uaPeer; + + /* Invalid authentication combinations */ + if (parsedline->conntype == ctLocal && + parsedline->auth_method == uaGSS) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("gssapi authentication is not supported on local sockets"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "gssapi authentication is not supported on local sockets"; + return NULL; + } + + if (parsedline->conntype != ctLocal && + parsedline->auth_method == uaPeer) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("peer authentication is only supported on local sockets"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "peer authentication is only supported on local sockets"; + return NULL; + } + + /* + * 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(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("cert authentication is only supported on hostssl connections"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "cert authentication is only supported on hostssl connections"; + return NULL; + } + + /* + * For GSS and SSPI, set the default value of include_realm to true. + * Having include_realm set to false is dangerous in multi-realm + * situations and is generally considered bad practice. We keep the + * capability around for backwards compatibility, but we might want to + * remove it at some point in the future. Users who still need to strip + * the realm off would be better served by using an appropriate regex in a + * pg_ident.conf mapping. + */ + if (parsedline->auth_method == uaGSS || + parsedline->auth_method == uaSSPI) + parsedline->include_realm = true; + + /* + * For SSPI, include_realm defaults to the SAM-compatible domain (aka + * NetBIOS name) and user names instead of the Kerberos principal name for + * compatibility. + */ + if (parsedline->auth_method == uaSSPI) + { + parsedline->compat_realm = true; + parsedline->upn_username = false; + } + + /* Parse remaining arguments */ + while ((field = lnext(tok_line->fields, field)) != NULL) + { + tokens = lfirst(field); + foreach(tokencell, tokens) + { + char *val; + + token = lfirst(tokencell); + + str = pstrdup(token->string); + val = strchr(str, '='); + if (val == NULL) + { + /* + * Got something that's not a name=value pair. + */ + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("authentication option not in name=value format: %s", token->string), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("authentication option not in name=value format: %s", + token->string); + return NULL; + } + + *val++ = '\0'; /* str now holds "name", val holds "value" */ + if (!parse_hba_auth_opt(str, val, parsedline, elevel, err_msg)) + /* parse_hba_auth_opt already logged the error message */ + return NULL; + pfree(str); + } + } + + /* + * Check if the selected authentication method has any mandatory arguments + * that are not set. + */ + if (parsedline->auth_method == uaLDAP) + { +#ifndef HAVE_LDAP_INITIALIZE + /* Not mandatory for OpenLDAP, because it can use DNS SRV records */ + MANDATORY_AUTH_ARG(parsedline->ldapserver, "ldapserver", "ldap"); +#endif + + /* + * 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 one of + * ldapsearchattribute or ldapsearchfilter. Disallow mixing these + * parameters. + */ + if (parsedline->ldapprefix || parsedline->ldapsuffix) + { + if (parsedline->ldapbasedn || + parsedline->ldapbinddn || + parsedline->ldapbindpasswd || + parsedline->ldapsearchattribute || + parsedline->ldapsearchfilter) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "cannot use ldapbasedn, ldapbinddn, ldapbindpasswd, ldapsearchattribute, ldapsearchfilter, or ldapurl together with ldapprefix"; + return NULL; + } + } + else if (!parsedline->ldapbasedn) + { + ereport(elevel, + (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))); + *err_msg = "authentication method \"ldap\" requires argument \"ldapbasedn\", \"ldapprefix\", or \"ldapsuffix\" to be set"; + return NULL; + } + + /* + * When using search+bind, you can either use a simple attribute + * (defaulting to "uid") or a fully custom search filter. You can't + * do both. + */ + if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("cannot use ldapsearchattribute together with ldapsearchfilter"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "cannot use ldapsearchattribute together with ldapsearchfilter"; + return NULL; + } + } + + if (parsedline->auth_method == uaRADIUS) + { + MANDATORY_AUTH_ARG(parsedline->radiusservers, "radiusservers", "radius"); + MANDATORY_AUTH_ARG(parsedline->radiussecrets, "radiussecrets", "radius"); + + if (list_length(parsedline->radiusservers) < 1) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("list of RADIUS servers cannot be empty"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "list of RADIUS servers cannot be empty"; + return NULL; + } + + if (list_length(parsedline->radiussecrets) < 1) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("list of RADIUS secrets cannot be empty"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "list of RADIUS secrets cannot be empty"; + return NULL; + } + + /* + * Verify length of option lists - each can be 0 (except for secrets, + * but that's already checked above), 1 (use the same value + * everywhere) or the same as the number of servers. + */ + if (!(list_length(parsedline->radiussecrets) == 1 || + list_length(parsedline->radiussecrets) == list_length(parsedline->radiusservers))) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)", + list_length(parsedline->radiussecrets), + list_length(parsedline->radiusservers)), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("the number of RADIUS secrets (%d) must be 1 or the same as the number of RADIUS servers (%d)", + list_length(parsedline->radiussecrets), + list_length(parsedline->radiusservers)); + return NULL; + } + if (!(list_length(parsedline->radiusports) == 0 || + list_length(parsedline->radiusports) == 1 || + list_length(parsedline->radiusports) == list_length(parsedline->radiusservers))) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)", + list_length(parsedline->radiusports), + list_length(parsedline->radiusservers)), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("the number of RADIUS ports (%d) must be 1 or the same as the number of RADIUS servers (%d)", + list_length(parsedline->radiusports), + list_length(parsedline->radiusservers)); + return NULL; + } + if (!(list_length(parsedline->radiusidentifiers) == 0 || + list_length(parsedline->radiusidentifiers) == 1 || + list_length(parsedline->radiusidentifiers) == list_length(parsedline->radiusservers))) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)", + list_length(parsedline->radiusidentifiers), + list_length(parsedline->radiusservers)), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("the number of RADIUS identifiers (%d) must be 1 or the same as the number of RADIUS servers (%d)", + list_length(parsedline->radiusidentifiers), + list_length(parsedline->radiusservers)); + return NULL; + } + } + + /* + * Enforce any parameters implied by other settings. + */ + if (parsedline->auth_method == uaCert) + { + /* + * For auth method cert, client certificate validation is mandatory, and it implies + * the level of verify-full. + */ + parsedline->clientcert = clientCertFull; + } + + return parsedline; +} + + +/* + * Parse one name-value pair as an authentication option into the given + * HbaLine. Return true if we successfully parse the option, false if we + * encounter an error. In the event of an error, also log a message at + * ereport level elevel, and store a message string into *err_msg. + */ +static bool +parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, + int elevel, char **err_msg) +{ + int line_num = hbaline->linenumber; + +#ifdef USE_LDAP + hbaline->ldapscope = LDAP_SCOPE_SUBTREE; +#endif + + if (strcmp(name, "map") == 0) + { + if (hbaline->auth_method != uaIdent && + hbaline->auth_method != uaPeer && + hbaline->auth_method != uaGSS && + hbaline->auth_method != uaSSPI && + hbaline->auth_method != uaCert) + INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, and cert")); + hbaline->usermap = pstrdup(val); + } + else if (strcmp(name, "clientcert") == 0) + { + if (hbaline->conntype != ctHostSSL) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("clientcert can only be configured for \"hostssl\" rows"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "clientcert can only be configured for \"hostssl\" rows"; + return false; + } + + if (strcmp(val, "verify-full") == 0) + { + hbaline->clientcert = clientCertFull; + } + else if (strcmp(val, "verify-ca") == 0) + { + if (hbaline->auth_method == uaCert) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication"; + return false; + } + + hbaline->clientcert = clientCertCA; + } + else + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid value for clientcert: \"%s\"", val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + return false; + } + } + else if (strcmp(name, "clientname") == 0) + { + if (hbaline->conntype != ctHostSSL) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("clientname can only be configured for \"hostssl\" rows"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "clientname can only be configured for \"hostssl\" rows"; + return false; + } + + if (strcmp(val, "CN") == 0) + { + hbaline->clientcertname = clientCertCN; + } + else if (strcmp(val, "DN") == 0) + { + hbaline->clientcertname = clientCertDN; + } + else + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid value for clientname: \"%s\"", val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + return false; + } + } + else if (strcmp(name, "pamservice") == 0) + { + REQUIRE_AUTH_OPTION(uaPAM, "pamservice", "pam"); + hbaline->pamservice = pstrdup(val); + } + else if (strcmp(name, "pam_use_hostname") == 0) + { + REQUIRE_AUTH_OPTION(uaPAM, "pam_use_hostname", "pam"); + if (strcmp(val, "1") == 0) + hbaline->pam_use_hostname = true; + else + hbaline->pam_use_hostname = false; + + } + else if (strcmp(name, "ldapurl") == 0) + { +#ifdef LDAP_API_FEATURE_X_OPENLDAP + LDAPURLDesc *urldata; + int rc; +#endif + + REQUIRE_AUTH_OPTION(uaLDAP, "ldapurl", "ldap"); +#ifdef LDAP_API_FEATURE_X_OPENLDAP + rc = ldap_url_parse(val, &urldata); + if (rc != LDAP_SUCCESS) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not parse LDAP URL \"%s\": %s", val, ldap_err2string(rc)))); + *err_msg = psprintf("could not parse LDAP URL \"%s\": %s", + val, ldap_err2string(rc)); + return false; + } + + if (strcmp(urldata->lud_scheme, "ldap") != 0 && + strcmp(urldata->lud_scheme, "ldaps") != 0) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("unsupported LDAP URL scheme: %s", urldata->lud_scheme))); + *err_msg = psprintf("unsupported LDAP URL scheme: %s", + urldata->lud_scheme); + ldap_free_urldesc(urldata); + return false; + } + + if (urldata->lud_scheme) + hbaline->ldapscheme = pstrdup(urldata->lud_scheme); + if (urldata->lud_host) + hbaline->ldapserver = pstrdup(urldata->lud_host); + hbaline->ldapport = urldata->lud_port; + if (urldata->lud_dn) + hbaline->ldapbasedn = pstrdup(urldata->lud_dn); + + if (urldata->lud_attrs) + hbaline->ldapsearchattribute = pstrdup(urldata->lud_attrs[0]); /* only use first one */ + hbaline->ldapscope = urldata->lud_scope; + if (urldata->lud_filter) + hbaline->ldapsearchfilter = pstrdup(urldata->lud_filter); + ldap_free_urldesc(urldata); +#else /* not OpenLDAP */ + ereport(elevel, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("LDAP URLs not supported on this platform"))); + *err_msg = "LDAP URLs not supported on this platform"; +#endif /* not OpenLDAP */ + } + else if (strcmp(name, "ldaptls") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldaptls", "ldap"); + if (strcmp(val, "1") == 0) + hbaline->ldaptls = true; + else + hbaline->ldaptls = false; + } + else if (strcmp(name, "ldapscheme") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapscheme", "ldap"); + if (strcmp(val, "ldap") != 0 && strcmp(val, "ldaps") != 0) + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid ldapscheme value: \"%s\"", val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + hbaline->ldapscheme = pstrdup(val); + } + else if (strcmp(name, "ldapserver") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap"); + hbaline->ldapserver = pstrdup(val); + } + else if (strcmp(name, "ldapport") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapport", "ldap"); + hbaline->ldapport = atoi(val); + if (hbaline->ldapport == 0) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid LDAP port number: \"%s\"", val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("invalid LDAP port number: \"%s\"", val); + return false; + } + } + else if (strcmp(name, "ldapbinddn") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapbinddn", "ldap"); + hbaline->ldapbinddn = pstrdup(val); + } + else if (strcmp(name, "ldapbindpasswd") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapbindpasswd", "ldap"); + hbaline->ldapbindpasswd = pstrdup(val); + } + else if (strcmp(name, "ldapsearchattribute") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchattribute", "ldap"); + hbaline->ldapsearchattribute = pstrdup(val); + } + else if (strcmp(name, "ldapsearchfilter") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapsearchfilter", "ldap"); + hbaline->ldapsearchfilter = pstrdup(val); + } + else if (strcmp(name, "ldapbasedn") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapbasedn", "ldap"); + hbaline->ldapbasedn = pstrdup(val); + } + else if (strcmp(name, "ldapprefix") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapprefix", "ldap"); + hbaline->ldapprefix = pstrdup(val); + } + else if (strcmp(name, "ldapsuffix") == 0) + { + REQUIRE_AUTH_OPTION(uaLDAP, "ldapsuffix", "ldap"); + hbaline->ldapsuffix = pstrdup(val); + } + else if (strcmp(name, "krb_realm") == 0) + { + if (hbaline->auth_method != uaGSS && + hbaline->auth_method != uaSSPI) + INVALID_AUTH_OPTION("krb_realm", gettext_noop("gssapi and sspi")); + hbaline->krb_realm = pstrdup(val); + } + else if (strcmp(name, "include_realm") == 0) + { + if (hbaline->auth_method != uaGSS && + hbaline->auth_method != uaSSPI) + INVALID_AUTH_OPTION("include_realm", gettext_noop("gssapi and sspi")); + if (strcmp(val, "1") == 0) + hbaline->include_realm = true; + else + hbaline->include_realm = false; + } + else if (strcmp(name, "compat_realm") == 0) + { + if (hbaline->auth_method != uaSSPI) + INVALID_AUTH_OPTION("compat_realm", gettext_noop("sspi")); + if (strcmp(val, "1") == 0) + hbaline->compat_realm = true; + else + hbaline->compat_realm = false; + } + else if (strcmp(name, "upn_username") == 0) + { + if (hbaline->auth_method != uaSSPI) + INVALID_AUTH_OPTION("upn_username", gettext_noop("sspi")); + if (strcmp(val, "1") == 0) + hbaline->upn_username = true; + else + hbaline->upn_username = false; + } + else if (strcmp(name, "radiusservers") == 0) + { + struct addrinfo *gai_result; + struct addrinfo hints; + int ret; + List *parsed_servers; + ListCell *l; + char *dupval = pstrdup(val); + + REQUIRE_AUTH_OPTION(uaRADIUS, "radiusservers", "radius"); + + if (!SplitGUCList(dupval, ',', &parsed_servers)) + { + /* syntax error in list */ + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not parse RADIUS server list \"%s\"", + val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + return false; + } + + /* For each entry in the list, translate it */ + foreach(l, parsed_servers) + { + MemSet(&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_DGRAM; + hints.ai_family = AF_UNSPEC; + + ret = pg_getaddrinfo_all((char *) lfirst(l), NULL, &hints, &gai_result); + if (ret || !gai_result) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not translate RADIUS server name \"%s\" to address: %s", + (char *) lfirst(l), gai_strerror(ret)), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + if (gai_result) + pg_freeaddrinfo_all(hints.ai_family, gai_result); + + list_free(parsed_servers); + return false; + } + pg_freeaddrinfo_all(hints.ai_family, gai_result); + } + + /* All entries are OK, so store them */ + hbaline->radiusservers = parsed_servers; + hbaline->radiusservers_s = pstrdup(val); + } + else if (strcmp(name, "radiusports") == 0) + { + List *parsed_ports; + ListCell *l; + char *dupval = pstrdup(val); + + REQUIRE_AUTH_OPTION(uaRADIUS, "radiusports", "radius"); + + if (!SplitGUCList(dupval, ',', &parsed_ports)) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not parse RADIUS port list \"%s\"", + val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("invalid RADIUS port number: \"%s\"", val); + return false; + } + + foreach(l, parsed_ports) + { + if (atoi(lfirst(l)) == 0) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("invalid RADIUS port number: \"%s\"", val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + + return false; + } + } + hbaline->radiusports = parsed_ports; + hbaline->radiusports_s = pstrdup(val); + } + else if (strcmp(name, "radiussecrets") == 0) + { + List *parsed_secrets; + char *dupval = pstrdup(val); + + REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecrets", "radius"); + + if (!SplitGUCList(dupval, ',', &parsed_secrets)) + { + /* syntax error in list */ + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not parse RADIUS secret list \"%s\"", + val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + return false; + } + + hbaline->radiussecrets = parsed_secrets; + hbaline->radiussecrets_s = pstrdup(val); + } + else if (strcmp(name, "radiusidentifiers") == 0) + { + List *parsed_identifiers; + char *dupval = pstrdup(val); + + REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifiers", "radius"); + + if (!SplitGUCList(dupval, ',', &parsed_identifiers)) + { + /* syntax error in list */ + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("could not parse RADIUS identifiers list \"%s\"", + val), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + return false; + } + + hbaline->radiusidentifiers = parsed_identifiers; + hbaline->radiusidentifiers_s = pstrdup(val); + } + else + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("unrecognized authentication option name: \"%s\"", + name), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = psprintf("unrecognized authentication option name: \"%s\"", + name); + return false; + } + return true; +} + +/* + * Scan the pre-parsed hba file, looking for a match to the port's connection + * request. + */ +static void +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_role_oid(port->user_name, true); + + 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 */ + if (port->ssl_in_use) + { + /* 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; + } + + /* Check GSSAPI state */ +#ifdef ENABLE_GSS + if (port->gss && port->gss->enc && + hba->conntype == ctHostNoGSS) + continue; + else if (!(port->gss && port->gss->enc) && + hba->conntype == ctHostGSS) + continue; +#else + if (hba->conntype == ctHostGSS) + continue; +#endif + + /* Check IP address */ + switch (hba->ip_cmp_method) + { + case ipCmpMask: + if (hba->hostname) + { + if (!check_hostname(port, + hba->hostname)) + continue; + } + else + { + if (!check_ip(&port->raddr, + (struct sockaddr *) &hba->addr, + (struct sockaddr *) &hba->mask)) + continue; + } + break; + case ipCmpAll: + 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->databases)) + continue; + + if (!check_role(port->user_name, roleid, hba->roles)) + continue; + + /* Found a record that matched! */ + port->hba = hba; + return; + } + + /* If no matching entry was found, then implicitly reject. */ + hba = palloc0(sizeof(HbaLine)); + hba->auth_method = uaImplicitReject; + port->hba = hba; +} + +/* + * 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. + * + * On a false result, caller will take care of reporting a FATAL error in case + * this is the initial startup. If it happens on reload, we just keep running + * with the old data. + */ +bool +load_hba(void) +{ + FILE *file; + List *hba_lines = NIL; + ListCell *line; + List *new_parsed_lines = NIL; + bool ok = true; + MemoryContext linecxt; + MemoryContext oldcxt; + MemoryContext hbacxt; + + file = AllocateFile(HbaFileName, "r"); + if (file == NULL) + { + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not open configuration file \"%s\": %m", + HbaFileName))); + return false; + } + + linecxt = tokenize_file(HbaFileName, file, &hba_lines, LOG); + FreeFile(file); + + /* Now parse all the lines */ + Assert(PostmasterContext); + hbacxt = AllocSetContextCreate(PostmasterContext, + "hba parser context", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(hbacxt); + foreach(line, hba_lines) + { + TokenizedLine *tok_line = (TokenizedLine *) lfirst(line); + HbaLine *newline; + + /* don't parse lines that already have errors */ + if (tok_line->err_msg != NULL) + { + ok = false; + continue; + } + + if ((newline = parse_hba_line(tok_line, LOG)) == NULL) + { + /* Parse error; remember there's trouble */ + ok = false; + + /* + * Keep parsing the rest of the file so we can report errors on + * more than the first line. Error has already been logged, no + * need for more chatter here. + */ + continue; + } + + new_parsed_lines = lappend(new_parsed_lines, newline); + } + + /* + * A valid HBA file must have at least one entry; else there's no way to + * connect to the postmaster. But only complain about this if we didn't + * already have parsing errors. + */ + if (ok && new_parsed_lines == NIL) + { + ereport(LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("configuration file \"%s\" contains no entries", + HbaFileName))); + ok = false; + } + + /* Free tokenizer memory */ + MemoryContextDelete(linecxt); + MemoryContextSwitchTo(oldcxt); + + if (!ok) + { + /* File contained one or more errors, so bail out */ + MemoryContextDelete(hbacxt); + return false; + } + + /* Loaded new file successfully, replace the one we use */ + if (parsed_hba_context != NULL) + MemoryContextDelete(parsed_hba_context); + parsed_hba_context = hbacxt; + parsed_hba_lines = new_parsed_lines; + + return true; +} + +/* + * This macro specifies the maximum number of authentication options + * that are possible with any given authentication method that is supported. + * Currently LDAP supports 11, and there are 3 that are not dependent on + * the auth method here. It may not actually be possible to set all of them + * at the same time, but we'll set the macro value high enough to be + * conservative and avoid warnings from static analysis tools. + */ +#define MAX_HBA_OPTIONS 14 + +/* + * Create a text array listing the options specified in the HBA line. + * Return NULL if no options are specified. + */ +static ArrayType * +gethba_options(HbaLine *hba) +{ + int noptions; + Datum options[MAX_HBA_OPTIONS]; + + noptions = 0; + + if (hba->auth_method == uaGSS || hba->auth_method == uaSSPI) + { + if (hba->include_realm) + options[noptions++] = + CStringGetTextDatum("include_realm=true"); + + if (hba->krb_realm) + options[noptions++] = + CStringGetTextDatum(psprintf("krb_realm=%s", hba->krb_realm)); + } + + if (hba->usermap) + options[noptions++] = + CStringGetTextDatum(psprintf("map=%s", hba->usermap)); + + if (hba->clientcert != clientCertOff) + options[noptions++] = + CStringGetTextDatum(psprintf("clientcert=%s", (hba->clientcert == clientCertCA) ? "verify-ca" : "verify-full")); + + if (hba->pamservice) + options[noptions++] = + CStringGetTextDatum(psprintf("pamservice=%s", hba->pamservice)); + + if (hba->auth_method == uaLDAP) + { + if (hba->ldapserver) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapserver=%s", hba->ldapserver)); + + if (hba->ldapport) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapport=%d", hba->ldapport)); + + if (hba->ldaptls) + options[noptions++] = + CStringGetTextDatum("ldaptls=true"); + + if (hba->ldapprefix) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapprefix=%s", hba->ldapprefix)); + + if (hba->ldapsuffix) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapsuffix=%s", hba->ldapsuffix)); + + if (hba->ldapbasedn) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapbasedn=%s", hba->ldapbasedn)); + + if (hba->ldapbinddn) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapbinddn=%s", hba->ldapbinddn)); + + if (hba->ldapbindpasswd) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapbindpasswd=%s", + hba->ldapbindpasswd)); + + if (hba->ldapsearchattribute) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapsearchattribute=%s", + hba->ldapsearchattribute)); + + if (hba->ldapsearchfilter) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapsearchfilter=%s", + hba->ldapsearchfilter)); + + if (hba->ldapscope) + options[noptions++] = + CStringGetTextDatum(psprintf("ldapscope=%d", hba->ldapscope)); + } + + if (hba->auth_method == uaRADIUS) + { + if (hba->radiusservers_s) + options[noptions++] = + CStringGetTextDatum(psprintf("radiusservers=%s", hba->radiusservers_s)); + + if (hba->radiussecrets_s) + options[noptions++] = + CStringGetTextDatum(psprintf("radiussecrets=%s", hba->radiussecrets_s)); + + if (hba->radiusidentifiers_s) + options[noptions++] = + CStringGetTextDatum(psprintf("radiusidentifiers=%s", hba->radiusidentifiers_s)); + + if (hba->radiusports_s) + options[noptions++] = + CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s)); + } + + /* If you add more options, consider increasing MAX_HBA_OPTIONS. */ + Assert(noptions <= MAX_HBA_OPTIONS); + + if (noptions > 0) + return construct_array(options, noptions, TEXTOID, -1, false, TYPALIGN_INT); + else + return NULL; +} + +/* Number of columns in pg_hba_file_rules view */ +#define NUM_PG_HBA_FILE_RULES_ATTS 9 + +/* + * fill_hba_line: build one row of pg_hba_file_rules view, add it to tuplestore + * + * tuple_store: where to store data + * tupdesc: tuple descriptor for the view + * lineno: pg_hba.conf line number (must always be valid) + * hba: parsed line data (can be NULL, in which case err_msg should be set) + * err_msg: error message (NULL if none) + * + * Note: leaks memory, but we don't care since this is run in a short-lived + * memory context. + */ +static void +fill_hba_line(Tuplestorestate *tuple_store, TupleDesc tupdesc, + int lineno, HbaLine *hba, const char *err_msg) +{ + Datum values[NUM_PG_HBA_FILE_RULES_ATTS]; + bool nulls[NUM_PG_HBA_FILE_RULES_ATTS]; + char buffer[NI_MAXHOST]; + HeapTuple tuple; + int index; + ListCell *lc; + const char *typestr; + const char *addrstr; + const char *maskstr; + ArrayType *options; + + Assert(tupdesc->natts == NUM_PG_HBA_FILE_RULES_ATTS); + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + index = 0; + + /* line_number */ + values[index++] = Int32GetDatum(lineno); + + if (hba != NULL) + { + /* type */ + /* Avoid a default: case so compiler will warn about missing cases */ + typestr = NULL; + switch (hba->conntype) + { + case ctLocal: + typestr = "local"; + break; + case ctHost: + typestr = "host"; + break; + case ctHostSSL: + typestr = "hostssl"; + break; + case ctHostNoSSL: + typestr = "hostnossl"; + break; + case ctHostGSS: + typestr = "hostgssenc"; + break; + case ctHostNoGSS: + typestr = "hostnogssenc"; + break; + } + if (typestr) + values[index++] = CStringGetTextDatum(typestr); + else + nulls[index++] = true; + + /* database */ + if (hba->databases) + { + /* + * Flatten HbaToken list to string list. It might seem that we + * should re-quote any quoted tokens, but that has been rejected + * on the grounds that it makes it harder to compare the array + * elements to other system catalogs. That makes entries like + * "all" or "samerole" formally ambiguous ... but users who name + * databases/roles that way are inflicting their own pain. + */ + List *names = NIL; + + foreach(lc, hba->databases) + { + HbaToken *tok = lfirst(lc); + + names = lappend(names, tok->string); + } + values[index++] = PointerGetDatum(strlist_to_textarray(names)); + } + else + nulls[index++] = true; + + /* user */ + if (hba->roles) + { + /* Flatten HbaToken list to string list; see comment above */ + List *roles = NIL; + + foreach(lc, hba->roles) + { + HbaToken *tok = lfirst(lc); + + roles = lappend(roles, tok->string); + } + values[index++] = PointerGetDatum(strlist_to_textarray(roles)); + } + else + nulls[index++] = true; + + /* address and netmask */ + /* Avoid a default: case so compiler will warn about missing cases */ + addrstr = maskstr = NULL; + switch (hba->ip_cmp_method) + { + case ipCmpMask: + if (hba->hostname) + { + addrstr = hba->hostname; + } + else + { + /* + * Note: if pg_getnameinfo_all fails, it'll set buffer to + * "???", which we want to return. + */ + if (hba->addrlen > 0) + { + if (pg_getnameinfo_all(&hba->addr, hba->addrlen, + buffer, sizeof(buffer), + NULL, 0, + NI_NUMERICHOST) == 0) + clean_ipv6_addr(hba->addr.ss_family, buffer); + addrstr = pstrdup(buffer); + } + if (hba->masklen > 0) + { + if (pg_getnameinfo_all(&hba->mask, hba->masklen, + buffer, sizeof(buffer), + NULL, 0, + NI_NUMERICHOST) == 0) + clean_ipv6_addr(hba->mask.ss_family, buffer); + maskstr = pstrdup(buffer); + } + } + break; + case ipCmpAll: + addrstr = "all"; + break; + case ipCmpSameHost: + addrstr = "samehost"; + break; + case ipCmpSameNet: + addrstr = "samenet"; + break; + } + if (addrstr) + values[index++] = CStringGetTextDatum(addrstr); + else + nulls[index++] = true; + if (maskstr) + values[index++] = CStringGetTextDatum(maskstr); + else + nulls[index++] = true; + + /* auth_method */ + values[index++] = CStringGetTextDatum(hba_authname(hba->auth_method)); + + /* options */ + options = gethba_options(hba); + if (options) + values[index++] = PointerGetDatum(options); + else + nulls[index++] = true; + } + else + { + /* no parsing result, so set relevant fields to nulls */ + memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool)); + } + + /* error */ + if (err_msg) + values[NUM_PG_HBA_FILE_RULES_ATTS - 1] = CStringGetTextDatum(err_msg); + else + nulls[NUM_PG_HBA_FILE_RULES_ATTS - 1] = true; + + tuple = heap_form_tuple(tupdesc, values, nulls); + tuplestore_puttuple(tuple_store, tuple); +} + +/* + * Read the pg_hba.conf file and fill the tuplestore with view records. + */ +static void +fill_hba_view(Tuplestorestate *tuple_store, TupleDesc tupdesc) +{ + FILE *file; + List *hba_lines = NIL; + ListCell *line; + MemoryContext linecxt; + MemoryContext hbacxt; + MemoryContext oldcxt; + + /* + * In the unlikely event that we can't open pg_hba.conf, we throw an + * error, rather than trying to report it via some sort of view entry. + * (Most other error conditions should result in a message in a view + * entry.) + */ + file = AllocateFile(HbaFileName, "r"); + if (file == NULL) + ereport(ERROR, + (errcode_for_file_access(), + errmsg("could not open configuration file \"%s\": %m", + HbaFileName))); + + linecxt = tokenize_file(HbaFileName, file, &hba_lines, DEBUG3); + FreeFile(file); + + /* Now parse all the lines */ + hbacxt = AllocSetContextCreate(CurrentMemoryContext, + "hba parser context", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(hbacxt); + foreach(line, hba_lines) + { + TokenizedLine *tok_line = (TokenizedLine *) lfirst(line); + HbaLine *hbaline = NULL; + + /* don't parse lines that already have errors */ + if (tok_line->err_msg == NULL) + hbaline = parse_hba_line(tok_line, DEBUG3); + + fill_hba_line(tuple_store, tupdesc, tok_line->line_num, + hbaline, tok_line->err_msg); + } + + /* Free tokenizer memory */ + MemoryContextDelete(linecxt); + /* Free parse_hba_line memory */ + MemoryContextSwitchTo(oldcxt); + MemoryContextDelete(hbacxt); +} + +/* + * SQL-accessible SRF to return all the entries in the pg_hba.conf file. + */ +Datum +pg_hba_file_rules(PG_FUNCTION_ARGS) +{ + Tuplestorestate *tuple_store; + TupleDesc tupdesc; + MemoryContext old_cxt; + ReturnSetInfo *rsi; + + /* + * We must use the Materialize mode to be safe against HBA file changes + * while the cursor is open. It's also more efficient than having to look + * up our current position in the parsed list every time. + */ + rsi = (ReturnSetInfo *) fcinfo->resultinfo; + + /* Check to see if caller supports us returning a tuplestore */ + if (rsi == NULL || !IsA(rsi, ReturnSetInfo)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("set-valued function called in context that cannot accept a set"))); + if (!(rsi->allowedModes & SFRM_Materialize)) + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("materialize mode required, but it is not allowed in this context"))); + + rsi->returnMode = SFRM_Materialize; + + /* Build a tuple descriptor for our result type */ + if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE) + elog(ERROR, "return type must be a row type"); + + /* Build tuplestore to hold the result rows */ + old_cxt = MemoryContextSwitchTo(rsi->econtext->ecxt_per_query_memory); + + tuple_store = + tuplestore_begin_heap(rsi->allowedModes & SFRM_Materialize_Random, + false, work_mem); + rsi->setDesc = tupdesc; + rsi->setResult = tuple_store; + + MemoryContextSwitchTo(old_cxt); + + /* Fill the tuplestore */ + fill_hba_view(tuple_store, tupdesc); + + PG_RETURN_NULL(); +} + + +/* + * Parse one tokenised line from the ident config file and store the result in + * an IdentLine structure. + * + * If parsing fails, log a message and return NULL. + * + * 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 IdentLine * +parse_ident_line(TokenizedLine *tok_line) +{ + int line_num = tok_line->line_num; + ListCell *field; + List *tokens; + HbaToken *token; + IdentLine *parsedline; + + Assert(tok_line->fields != NIL); + field = list_head(tok_line->fields); + + parsedline = palloc0(sizeof(IdentLine)); + parsedline->linenumber = line_num; + + /* Get the map token (must exist) */ + tokens = lfirst(field); + IDENT_MULTI_VALUE(tokens); + token = linitial(tokens); + parsedline->usermap = pstrdup(token->string); + + /* Get the ident user token */ + field = lnext(tok_line->fields, field); + IDENT_FIELD_ABSENT(field); + tokens = lfirst(field); + IDENT_MULTI_VALUE(tokens); + token = linitial(tokens); + parsedline->ident_user = pstrdup(token->string); + + /* Get the PG rolename token */ + field = lnext(tok_line->fields, field); + IDENT_FIELD_ABSENT(field); + tokens = lfirst(field); + IDENT_MULTI_VALUE(tokens); + token = linitial(tokens); + parsedline->pg_role = pstrdup(token->string); + + 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 (identLine->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; + regmatch_t matches[2]; + pg_wchar *wstr; + int wlen; + char *ofs; + char *regexp_pgrole; + + wstr = palloc((strlen(ident_user) + 1) * sizeof(pg_wchar)); + wlen = pg_mb2wchar_with_len(ident_user, wstr, strlen(ident_user)); + + r = pg_regexec(&identLine->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, &identLine->re, errstr, sizeof(errstr)); + ereport(LOG, + (errcode(ERRCODE_INVALID_REGULAR_EXPRESSION), + errmsg("regular expression match for \"%s\" failed: %s", + identLine->ident_user + 1, errstr))); + *error_p = true; + } + + pfree(wstr); + return; + } + pfree(wstr); + + if ((ofs = strstr(identLine->pg_role, "\\1")) != NULL) + { + int offset; + + /* 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\"", + identLine->ident_user + 1, identLine->pg_role))); + *error_p = true; + return; + } + + /* + * length: original length minus length of \1 plus length of match + * plus null terminator + */ + regexp_pgrole = palloc0(strlen(identLine->pg_role) - 2 + (matches[1].rm_eo - matches[1].rm_so) + 1); + offset = ofs - identLine->pg_role; + memcpy(regexp_pgrole, identLine->pg_role, offset); + memcpy(regexp_pgrole + offset, + 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(identLine->pg_role); + } + + /* + * 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(identLine->pg_role, pg_role) == 0 && + pg_strcasecmp(identLine->ident_user, ident_user) == 0) + *found_p = true; + } + else + { + if (strcmp(identLine->pg_role, pg_role) == 0 && + strcmp(identLine->ident_user, ident_user) == 0) + *found_p = true; + } + } +} + + +/* + * Scan the (pre-parsed) ident usermap file line by line, looking for a match + * + * See if the user with ident username "auth_user" is allowed to act + * as Postgres user "pg_role" 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 wherein "pg_role" must be identical to + * "auth_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", + pg_role, auth_user))); + return STATUS_ERROR; + } + else + { + ListCell *line_cell; + + foreach(line_cell, parsed_ident_lines) + { + check_ident_usermap(lfirst(line_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 \"%s\" for user \"%s\" authenticated as \"%s\"", + usermap_name, pg_role, auth_user))); + } + return found_entry ? STATUS_OK : STATUS_ERROR; +} + + +/* + * Read the ident config file and create a List of IdentLine records for + * the contents. + * + * This works the same as load_hba(), but for the user config file. + */ +bool +load_ident(void) +{ + FILE *file; + List *ident_lines = NIL; + ListCell *line_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) + { + /* not fatal ... we just won't do any special ident maps */ + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not open usermap file \"%s\": %m", + IdentFileName))); + return false; + } + + linecxt = tokenize_file(IdentFileName, file, &ident_lines, LOG); + FreeFile(file); + + /* Now parse all the lines */ + Assert(PostmasterContext); + ident_context = AllocSetContextCreate(PostmasterContext, + "ident parser context", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(ident_context); + foreach(line_cell, ident_lines) + { + TokenizedLine *tok_line = (TokenizedLine *) lfirst(line_cell); + + /* don't parse lines that already have errors */ + if (tok_line->err_msg != NULL) + { + ok = false; + continue; + } + + if ((newline = parse_ident_line(tok_line)) == NULL) + { + /* Parse error; remember there's trouble */ + ok = false; + + /* + * Keep parsing the rest of the file so we can report errors on + * more than the first line. Error has already been logged, no + * need for more chatter 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, 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(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 != NIL) + { + foreach(parsed_line_cell, parsed_ident_lines) + { + newline = (IdentLine *) lfirst(parsed_line_cell); + if (newline->ident_user[0] == '/') + pg_regfree(&newline->re); + } + } + if (parsed_ident_context != NULL) + MemoryContextDelete(parsed_ident_context); + + parsed_ident_context = ident_context; + parsed_ident_lines = new_parsed_lines; + + return true; +} + + + +/* + * 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. + * + * If the file does not contain any entry matching the request, we return + * method = uaImplicitReject. + */ +void +hba_getauthmethod(hbaPort *port) +{ + check_hba(port); +} + + +/* + * Return the name of the auth method in use ("gss", "md5", "trust", etc.). + * + * The return value is statically allocated (see the UserAuthName array) and + * should not be freed. + */ +const char * +hba_authname(UserAuth auth_method) +{ + /* + * Make sure UserAuthName[] tracks additions to the UserAuth enum + */ + StaticAssertStmt(lengthof(UserAuthName) == USER_AUTH_LAST + 1, + "UserAuthName[] must match the UserAuth enum"); + + return UserAuthName[auth_method]; +} |