From c2b5168608e34acb171e0da6f4e9e16c14f82adb Mon Sep 17 00:00:00 2001 From: Julien Rouhaud Date: Mon, 28 Feb 2022 16:50:00 +0800 Subject: [PATCH v2 1/4] Extract view processing code from hba.c This file is already quite big and a later commit will add an additional view, so let's move all the view related code in hba.c into a new adt/hbafuncs.c. Author: Julien Rouhaud Reviewed-by: FIXME Discussion: https://postgr.es/m/20220223045959.35ipdsvbxcstrhya%40jrouhaud --- src/backend/libpq/hba.c | 3618 +++++++++++++----------------- src/backend/utils/adt/Makefile | 1 + src/backend/utils/adt/hbafuncs.c | 452 ++++ src/include/libpq/hba.h | 29 + src/tools/pgindent/typedefs.list | 2 +- 5 files changed, 2067 insertions(+), 2035 deletions(-) create mode 100644 src/backend/utils/adt/hbafuncs.c diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index d84a40b726..184df4942a 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -68,32 +68,6 @@ typedef struct 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. @@ -138,16 +112,10 @@ static const char *const UserAuthName[] = }; -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); /* @@ -419,7 +387,7 @@ tokenize_inc_file(List *tokens, } /* There is possible recursion here if the file contains @ */ - linecxt = tokenize_file(inc_fullname, inc_file, &inc_lines, elevel); + linecxt = tokenize_auth_file(inc_fullname, inc_file, &inc_lines, elevel); FreeFile(inc_file); pfree(inc_fullname); @@ -427,7 +395,7 @@ tokenize_inc_file(List *tokens, /* 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); + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(inc_line); ListCell *inc_field; /* If any line has an error, propagate that up to caller */ @@ -455,122 +423,6 @@ tokenize_inc_file(List *tokens, 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, NULL)) - { - /* 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? @@ -950,2217 +802,1915 @@ do { \ } 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. + * 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 HbaLine * -parse_hba_line(TokenizedLine *tok_line, int elevel) +static bool +parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, + int elevel, char **err_msg) { - 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; + int line_num = hbaline->linenumber; - parsedline = palloc0(sizeof(HbaLine)); - parsedline->linenumber = line_num; - parsedline->rawline = pstrdup(tok_line->raw_line); +#ifdef USE_LDAP + hbaline->ldapscope = LDAP_SCOPE_SUBTREE; +#endif - /* 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) + if (strcmp(name, "map") == 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 + 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(token->string, "host") == 0 || - strcmp(token->string, "hostssl") == 0 || - strcmp(token->string, "hostnossl") == 0 || - strcmp(token->string, "hostgssenc") == 0 || - strcmp(token->string, "hostnogssenc") == 0) + 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 (token->string[4] == 's') /* "hostssl" */ + if (strcmp(val, "verify-full") == 0) { - parsedline->conntype = ctHostSSL; - /* Log a warning if SSL support is not active */ -#ifdef USE_SSL - if (!EnableSSL) + hbaline->clientcert = clientCertFull; + } + else if (strcmp(val, "verify-ca") == 0) + { + if (hbaline->auth_method == uaCert) { ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("hostssl record cannot match because SSL is disabled"), - errhint("Set ssl = on in postgresql.conf."), + errmsg("clientcert only accepts \"verify-full\" when using \"cert\" authentication"), errcontext("line %d of configuration file \"%s\"", line_num, HbaFileName))); - *err_msg = "hostssl record cannot match because SSL is disabled"; + *err_msg = "clientcert can only be set to \"verify-full\" when using \"cert\" authentication"; + return false; } -#else + + hbaline->clientcert = clientCertCA; + } + 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."), + errmsg("invalid value for clientcert: \"%s\"", val), 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 + return false; } - else if (token->string[4] == 'g') /* "hostgssenc" */ + } + else if (strcmp(name, "clientname") == 0) + { + if (hbaline->conntype != ctHostSSL) { - 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."), + errmsg("clientname can only be configured for \"hostssl\" rows"), 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 + *err_msg = "clientname can only be configured for \"hostssl\" rows"; + return false; } - 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 + + if (strcmp(val, "CN") == 0) { - /* "host" */ - parsedline->conntype = ctHost; + hbaline->clientcertname = clientCertCN; } - } /* record type */ - else + 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) { - 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; + 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; - /* Get the databases. */ - field = lnext(tok_line->fields, field); - if (!field) + } + 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_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; + (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 */ } - parsedline->databases = NIL; - tokens = lfirst(field); - foreach(tokencell, tokens) + else if (strcmp(name, "ldaptls") == 0) { - parsedline->databases = lappend(parsedline->databases, - copy_hba_token(lfirst(tokencell))); + REQUIRE_AUTH_OPTION(uaLDAP, "ldaptls", "ldap"); + if (strcmp(val, "1") == 0) + hbaline->ldaptls = true; + else + hbaline->ldaptls = false; } - - /* Get the roles. */ - field = lnext(tok_line->fields, field); - if (!field) + else if (strcmp(name, "ldapscheme") == 0) { - 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; + 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); } - parsedline->roles = NIL; - tokens = lfirst(field); - foreach(tokencell, tokens) + else if (strcmp(name, "ldapserver") == 0) { - parsedline->roles = lappend(parsedline->roles, - copy_hba_token(lfirst(tokencell))); + REQUIRE_AUTH_OPTION(uaLDAP, "ldapserver", "ldap"); + hbaline->ldapserver = pstrdup(val); } - - if (parsedline->conntype != ctLocal) + else if (strcmp(name, "ldapport") == 0) { - /* Read the IP address field. (with or without CIDR netmask) */ - field = lnext(tok_line->fields, field); - if (!field) + REQUIRE_AUTH_OPTION(uaLDAP, "ldapport", "ldap"); + hbaline->ldapport = atoi(val); + if (hbaline->ldapport == 0) { ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("end-of-line before IP address specification"), + errmsg("invalid LDAP port number: \"%s\"", val), errcontext("line %d of configuration file \"%s\"", line_num, HbaFileName))); - *err_msg = "end-of-line before IP address specification"; - return NULL; + *err_msg = psprintf("invalid LDAP port number: \"%s\"", val); + return false; } - tokens = lfirst(field); - if (tokens->length > 1) + } + 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("multiple values specified for host address"), - errhint("Specify one address range per line."), + errmsg("could not parse RADIUS server list \"%s\"", + val), errcontext("line %d of configuration file \"%s\"", line_num, HbaFileName))); - *err_msg = "multiple values specified for host address"; - return NULL; + return false; } - token = linitial(tokens); - if (token_is_keyword(token, "all")) - { - parsedline->ip_cmp_method = ipCmpAll; - } - else if (token_is_keyword(token, "samehost")) + /* For each entry in the list, translate it */ + foreach(l, parsed_servers) { - /* 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; + MemSet(&hints, 0, sizeof(hints)); + hints.ai_socktype = SOCK_DGRAM; 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 + ret = pg_getaddrinfo_all((char *) lfirst(l), NULL, &hints, &gai_result); + if (ret || !gai_result) { ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("invalid IP address \"%s\": %s", - str, gai_strerror(ret)), + 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))); - *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; - } + list_free(parsed_servers); + return false; + } 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; - } + /* 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); - 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); + REQUIRE_AUTH_OPTION(uaRADIUS, "radiusports", "radius"); - 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; - } + 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; + } - 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); + 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))); - 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; - } + return false; } } - } /* != 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; + hbaline->radiusports = parsed_ports; + hbaline->radiusports_s = pstrdup(val); } - tokens = lfirst(field); - if (tokens->length > 1) + else if (strcmp(name, "radiussecrets") == 0) { - 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); + List *parsed_secrets; + char *dupval = pstrdup(val); - 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) + REQUIRE_AUTH_OPTION(uaRADIUS, "radiussecrets", "radius"); + + if (!SplitGUCList(dupval, ',', &parsed_secrets)) { + /* syntax error in list */ ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("MD5 authentication is not supported when \"db_user_namespace\" is enabled"), + errmsg("could not parse RADIUS secret list \"%s\"", + val), 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; + return false; } - parsedline->auth_method = uaMD5; + + hbaline->radiussecrets = parsed_secrets; + hbaline->radiussecrets_s = pstrdup(val); } - 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 + else if (strcmp(name, "radiusidentifiers") == 0) { - 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; - } + List *parsed_identifiers; + char *dupval = pstrdup(val); - 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; - } + REQUIRE_AUTH_OPTION(uaRADIUS, "radiusidentifiers", "radius"); - /* - * 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; + 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; + } - /* 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; + hbaline->radiusidentifiers = parsed_identifiers; + hbaline->radiusidentifiers_s = pstrdup(val); } - - if (parsedline->conntype != ctLocal && - parsedline->auth_method == uaPeer) + else { ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("peer authentication is only supported on local sockets"), + errmsg("unrecognized authentication option name: \"%s\"", + name), errcontext("line %d of configuration file \"%s\"", line_num, HbaFileName))); - *err_msg = "peer authentication is only supported on local sockets"; - return NULL; + *err_msg = psprintf("unrecognized authentication option name: \"%s\"", + name); + return false; } + return true; +} - /* - * 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; - } +/* + * 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; - /* - * 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; + /* Get the target role's OID. Note we do not error out for bad role. */ + roleid = get_role_oid(port->user_name, 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) + foreach(line, parsed_hba_lines) { - parsedline->compat_realm = true; - parsedline->upn_username = false; - } + hba = (HbaLine *) lfirst(line); - /* Parse remaining arguments */ - while ((field = lnext(tok_line->fields, field)) != NULL) - { - tokens = lfirst(field); - foreach(tokencell, tokens) + /* Check connection type */ + if (hba->conntype == ctLocal) { - char *val; - - token = lfirst(tokencell); + if (port->raddr.addr.ss_family != AF_UNIX) + continue; + } + else + { + if (port->raddr.addr.ss_family == AF_UNIX) + continue; - str = pstrdup(token->string); - val = strchr(str, '='); - if (val == NULL) + /* Check SSL state */ + if (port->ssl_in_use) { - /* - * 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; + /* 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; } - *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"); + /* 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 - /* - * 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) + /* Check IP address */ + switch (hba->ip_cmp_method) { - 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; + 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; } - } - 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; - } + } /* != ctLocal */ - /* - * 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; - } + /* 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 (parsedline->auth_method == uaRADIUS) + /* 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) { - MANDATORY_AUTH_ARG(parsedline->radiusservers, "radiusservers", "radius"); - MANDATORY_AUTH_ARG(parsedline->radiussecrets, "radiussecrets", "radius"); + ereport(LOG, + (errcode_for_file_access(), + errmsg("could not open configuration file \"%s\": %m", + HbaFileName))); + return false; + } - 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; - } + linecxt = tokenize_auth_file(HbaFileName, file, &hba_lines, LOG); + FreeFile(file); - 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; - } + /* Now parse all the lines */ + Assert(PostmasterContext); + hbacxt = AllocSetContextCreate(PostmasterContext, + "hba parser context", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(hbacxt); + foreach(line, hba_lines) + { + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line); + HbaLine *newline; - /* - * 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))) + /* don't parse lines that already have errors */ + if (tok_line->err_msg != NULL) { - 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; + ok = false; + continue; } - if (!(list_length(parsedline->radiusidentifiers) == 0 || - list_length(parsedline->radiusidentifiers) == 1 || - list_length(parsedline->radiusidentifiers) == list_length(parsedline->radiusservers))) + + if ((newline = parse_hba_line(tok_line, LOG)) == NULL) { - 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; + /* 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); } /* - * Enforce any parameters implied by other settings. + * 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 (parsedline->auth_method == uaCert) + if (ok && new_parsed_lines == NIL) { - /* - * For auth method cert, client certificate validation is mandatory, and it implies - * the level of verify-full. - */ - parsedline->clientcert = clientCertFull; + ereport(LOG, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("configuration file \"%s\" contains no entries", + HbaFileName))); + ok = false; } - return parsedline; -} + /* 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; +} /* - * 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. + * 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 bool -parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline, - int elevel, char **err_msg) +static IdentLine * +parse_ident_line(TokenizedAuthLine *tok_line) { - int line_num = hbaline->linenumber; + int line_num = tok_line->line_num; + ListCell *field; + List *tokens; + HbaToken *token; + IdentLine *parsedline; -#ifdef USE_LDAP - hbaline->ldapscope = LDAP_SCOPE_SUBTREE; -#endif + Assert(tok_line->fields != NIL); + field = list_head(tok_line->fields); - 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; - } + parsedline = palloc0(sizeof(IdentLine)); + parsedline->linenumber = line_num; - if (strcmp(val, "verify-full") == 0) + /* 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) { - hbaline->clientcert = clientCertFull; + 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; } - else if (strcmp(val, "verify-ca") == 0) + 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) { - if (hbaline->auth_method == uaCert) + char errstr[100]; + + if (r != REG_NOMATCH) { - 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; + /* 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; } - hbaline->clientcert = clientCertCA; + pfree(wstr); + return; } - else + pfree(wstr); + + if ((ofs = strstr(identLine->pg_role, "\\1")) != NULL) { - 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; + 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 if (strcmp(name, "clientname") == 0) - { - if (hbaline->conntype != ctHostSSL) + else { - 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; + /* no substitution, so copy the match */ + regexp_pgrole = pstrdup(identLine->pg_role); } - if (strcmp(val, "CN") == 0) - { - hbaline->clientcertname = clientCertCN; - } - else if (strcmp(val, "DN") == 0) + /* + * now check if the username actually matched what the user is trying + * to connect as + */ + if (case_insensitive) { - hbaline->clientcertname = clientCertDN; + if (pg_strcasecmp(regexp_pgrole, pg_role) == 0) + *found_p = true; } 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; + if (strcmp(regexp_pgrole, pg_role) == 0) + *found_p = true; } - } - 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; + pfree(regexp_pgrole); + return; } - else if (strcmp(name, "ldapurl") == 0) + else { -#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) + /* Not regular expression, so make complete match */ + if (case_insensitive) { - 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 (pg_strcasecmp(identLine->pg_role, pg_role) == 0 && + pg_strcasecmp(identLine->ident_user, ident_user) == 0) + *found_p = true; } - - if (strcmp(urldata->lud_scheme, "ldap") != 0 && - strcmp(urldata->lud_scheme, "ldaps") != 0) + else { - 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 (strcmp(identLine->pg_role, pg_role) == 0 && + strcmp(identLine->ident_user, ident_user) == 0) + *found_p = true; } + } +} - 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) +/* + * 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') { - REQUIRE_AUTH_OPTION(uaLDAP, "ldaptls", "ldap"); - if (strcmp(val, "1") == 0) - hbaline->ldaptls = true; + if (case_insensitive) + { + if (pg_strcasecmp(pg_role, auth_user) == 0) + return STATUS_OK; + } 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; + 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 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) + else { - 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; + 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; + } } - else if (strcmp(name, "compat_realm") == 0) + if (!found_entry && !error) { - 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; + ereport(LOG, + (errmsg("no match in usermap \"%s\" for user \"%s\" authenticated as \"%s\"", + usermap_name, pg_role, auth_user))); } - else if (strcmp(name, "upn_username") == 0) + 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) { - 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; + /* 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; } - 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"); + linecxt = tokenize_auth_file(IdentFileName, file, &ident_lines, LOG); + FreeFile(file); - if (!SplitGUCList(dupval, ',', &parsed_servers)) + /* 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) + { + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) lfirst(line_cell); + + /* don't parse lines that already have errors */ + if (tok_line->err_msg != NULL) { - /* 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; + ok = false; + continue; } - /* For each entry in the list, translate it */ - foreach(l, parsed_servers) + if ((newline = parse_ident_line(tok_line)) == NULL) { - 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); + /* Parse error; remember there's trouble */ + ok = false; - list_free(parsed_servers); - return false; - } - pg_freeaddrinfo_all(hints.ai_family, gai_result); + /* + * 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; } - /* All entries are OK, so store them */ - hbaline->radiusservers = parsed_servers; - hbaline->radiusservers_s = pstrdup(val); + new_parsed_lines = lappend(new_parsed_lines, newline); } - else if (strcmp(name, "radiusports") == 0) - { - List *parsed_ports; - ListCell *l; - char *dupval = pstrdup(val); - REQUIRE_AUTH_OPTION(uaRADIUS, "radiusports", "radius"); + /* Free tokenizer memory */ + MemoryContextDelete(linecxt); + MemoryContextSwitchTo(oldcxt); - if (!SplitGUCList(dupval, ',', &parsed_ports)) + 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) { - 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; + newline = (IdentLine *) lfirst(parsed_line_cell); + if (newline->ident_user[0] == '/') + pg_regfree(&newline->re); } + MemoryContextDelete(ident_context); + return false; + } - foreach(l, parsed_ports) + /* Loaded new file successfully, replace the one we use */ + if (parsed_ident_lines != NIL) + { + foreach(parsed_line_cell, parsed_ident_lines) { - if (atoi(lfirst(l)) == 0) + 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]; +} + +/* + * 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. + */ +HbaLine * +parse_hba_line(TokenizedAuthLine *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("invalid RADIUS port number: \"%s\"", val), + 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))); - - return false; + *err_msg = "hostssl record cannot match because SSL is disabled"; } - } - 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 */ +#else ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("could not parse RADIUS secret list \"%s\"", - val), + 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))); - return false; + *err_msg = "hostssl record cannot match because SSL is not supported by this build"; +#endif } - - 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)) + else if (token->string[4] == 'g') /* "hostgssenc" */ { - /* syntax error in list */ + parsedline->conntype = ctHostGSS; +#ifndef ENABLE_GSS ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("could not parse RADIUS identifiers list \"%s\"", - val), + 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))); - return false; + *err_msg = "hostgssenc record cannot match because GSSAPI is not supported by this build"; +#endif } - - hbaline->radiusidentifiers = parsed_identifiers; - hbaline->radiusidentifiers_s = pstrdup(val); - } + 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("unrecognized authentication option name: \"%s\"", - name), + errmsg("invalid connection type \"%s\"", + token->string), errcontext("line %d of configuration file \"%s\"", line_num, HbaFileName))); - *err_msg = psprintf("unrecognized authentication option name: \"%s\"", - name); - return false; + *err_msg = psprintf("invalid connection type \"%s\"", token->string); + return NULL; } - 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) + /* Get the databases. */ + field = lnext(tok_line->fields, field); + if (!field) { - hba = (HbaLine *) lfirst(line); - - /* Check connection type */ - if (hba->conntype == ctLocal) - { - if (port->raddr.addr.ss_family != AF_UNIX) - continue; - } - else - { - if (port->raddr.addr.ss_family == AF_UNIX) - 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, + ereport(elevel, (errcode(ERRCODE_CONFIG_FILE_ERROR), - errmsg("configuration file \"%s\" contains no entries", - HbaFileName))); - ok = false; + 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; } - - /* Free tokenizer memory */ - MemoryContextDelete(linecxt); - MemoryContextSwitchTo(oldcxt); - - if (!ok) + parsedline->databases = NIL; + tokens = lfirst(field); + foreach(tokencell, tokens) { - /* File contained one or more errors, so bail out */ - MemoryContextDelete(hbacxt); - return false; + parsedline->databases = lappend(parsedline->databases, + copy_hba_token(lfirst(tokencell))); } - /* 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) + /* Get the roles. */ + field = lnext(tok_line->fields, field); + if (!field) { - if (hba->include_realm) - options[noptions++] = - CStringGetTextDatum("include_realm=true"); - - if (hba->krb_realm) - options[noptions++] = - CStringGetTextDatum(psprintf("krb_realm=%s", hba->krb_realm)); + 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; } - - 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) + parsedline->roles = NIL; + tokens = lfirst(field); + foreach(tokencell, tokens) { - 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)); + parsedline->roles = lappend(parsedline->roles, + copy_hba_token(lfirst(tokencell))); } - if (hba->auth_method == uaRADIUS) + if (parsedline->conntype != ctLocal) { - 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)); + /* 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 (hba->radiusports_s) - options[noptions++] = - CStringGetTextDatum(psprintf("radiusports=%s", hba->radiusports_s)); - } + 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; - /* If you add more options, consider increasing MAX_HBA_OPTIONS. */ - Assert(noptions <= MAX_HBA_OPTIONS); + /* need a modifiable copy of token */ + str = pstrdup(token->string); - if (noptions > 0) - return construct_array(options, noptions, TEXTOID, -1, false, TYPALIGN_INT); - else - return NULL; -} + /* Check if it has a CIDR suffix and if so isolate it */ + cidr_slash = strchr(str, '/'); + if (cidr_slash) + *cidr_slash = '\0'; -/* Number of columns in pg_hba_file_rules view */ -#define NUM_PG_HBA_FILE_RULES_ATTS 9 + /* 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; -/* - * 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; + 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; + } - /* 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; + pg_freeaddrinfo_all(hints.ai_family, gai_result); - foreach(lc, hba->databases) + /* Get the netmask */ + if (cidr_slash) { - HbaToken *tok = lfirst(lc); + 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; + } - names = lappend(names, tok->string); + 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); } - values[index++] = PointerGetDatum(strlist_to_textarray(names)); - } - else - nulls[index++] = true; + 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); - /* user */ - if (hba->roles) - { - /* Flatten HbaToken list to string list; see comment above */ - List *roles = NIL; + 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; + } - foreach(lc, hba->roles) - { - HbaToken *tok = lfirst(lc); + 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); - roles = lappend(roles, tok->string); + 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; + } } - values[index++] = PointerGetDatum(strlist_to_textarray(roles)); } - else - nulls[index++] = true; + } /* != ctLocal */ - /* address and netmask */ - /* Avoid a default: case so compiler will warn about missing cases */ - addrstr = maskstr = NULL; - switch (hba->ip_cmp_method) + /* 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) { - 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; + 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; } - 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; + 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 { - /* no parsing result, so set relevant fields to nulls */ - memset(&nulls[1], true, (NUM_PG_HBA_FILE_RULES_ATTS - 2) * sizeof(bool)); + 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; } - /* 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; + 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; + } /* - * 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.) + * XXX: When using ident on local connections, change it to peer, for + * backwards compatibility. */ - 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); + if (parsedline->conntype == ctLocal && + parsedline->auth_method == uaIdent) + parsedline->auth_method = uaPeer; - /* Now parse all the lines */ - hbacxt = AllocSetContextCreate(CurrentMemoryContext, - "hba parser context", - ALLOCSET_SMALL_SIZES); - oldcxt = MemoryContextSwitchTo(hbacxt); - foreach(line, hba_lines) + /* Invalid authentication combinations */ + if (parsedline->conntype == ctLocal && + parsedline->auth_method == uaGSS) { - 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); + 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; } - /* 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; + 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; + } /* - * 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. + * SSPI authentication can never be enabled on ctLocal connections, + * because it's only supported on Windows, where ctLocal isn't supported. */ - 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] == '/') + if (parsedline->conntype != ctHostSSL && + parsedline->auth_method == uaCert) { - /* - * 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; + 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; + } - if (strcmp(identLine->usermap, usermap_name) != 0) - /* Line does not match the map name we're looking for, so just abort */ - return; + /* + * 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; - /* Match? */ - if (identLine->ident_user[0] == '/') + /* + * 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) { - /* - * 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)); + parsedline->compat_realm = true; + parsedline->upn_username = false; + } - r = pg_regexec(&identLine->re, wstr, wlen, 0, NULL, 2, matches, 0); - if (r) + /* Parse remaining arguments */ + while ((field = lnext(tok_line->fields, field)) != NULL) + { + tokens = lfirst(field); + foreach(tokencell, tokens) { - char errstr[100]; + char *val; - if (r != REG_NOMATCH) + token = lfirst(tokencell); + + str = pstrdup(token->string); + val = strchr(str, '='); + if (val == NULL) { - /* 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; + /* + * 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; } - pfree(wstr); - return; + *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); } - pfree(wstr); + } - if ((ofs = strstr(identLine->pg_role, "\\1")) != NULL) - { - int offset; + /* + * 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 - /* substitution of the first argument requested */ - if (matches[1].rm_so < 0) + /* + * 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(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; + 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; } - - /* - * 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 + else if (!parsedline->ldapbasedn) { - /* no substitution, so copy the match */ - regexp_pgrole = pstrdup(identLine->pg_role); + 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; } /* - * now check if the username actually matched what the user is trying - * to connect as + * 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 (case_insensitive) - { - if (pg_strcasecmp(regexp_pgrole, pg_role) == 0) - *found_p = true; - } - else + if (parsedline->ldapsearchattribute && parsedline->ldapsearchfilter) { - if (strcmp(regexp_pgrole, pg_role) == 0) - *found_p = true; + 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; } - pfree(regexp_pgrole); - - return; } - else + + if (parsedline->auth_method == uaRADIUS) { - /* Not regular expression, so make complete match */ - if (case_insensitive) + MANDATORY_AUTH_ARG(parsedline->radiusservers, "radiusservers", "radius"); + MANDATORY_AUTH_ARG(parsedline->radiussecrets, "radiussecrets", "radius"); + + if (list_length(parsedline->radiusservers) < 1) { - if (pg_strcasecmp(identLine->pg_role, pg_role) == 0 && - pg_strcasecmp(identLine->ident_user, ident_user) == 0) - *found_p = true; + 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; } - else + + if (list_length(parsedline->radiussecrets) < 1) { - if (strcmp(identLine->pg_role, pg_role) == 0 && - strcmp(identLine->ident_user, ident_user) == 0) - *found_p = true; + 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; } - } -} - - -/* - * 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) + /* + * 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))) { - if (pg_strcasecmp(pg_role, auth_user) == 0) - return STATUS_OK; + 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; } - else + if (!(list_length(parsedline->radiusports) == 0 || + list_length(parsedline->radiusports) == 1 || + list_length(parsedline->radiusports) == list_length(parsedline->radiusservers))) { - if (strcmp(pg_role, auth_user) == 0) - return STATUS_OK; + 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; } - 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) + if (!(list_length(parsedline->radiusidentifiers) == 0 || + list_length(parsedline->radiusidentifiers) == 1 || + list_length(parsedline->radiusidentifiers) == list_length(parsedline->radiusservers))) { - check_ident_usermap(lfirst(line_cell), usermap_name, - pg_role, auth_user, case_insensitive, - &found_entry, &error); - if (found_entry || error) - break; + 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; } } - if (!found_entry && !error) + + /* + * Enforce any parameters implied by other settings. + */ + if (parsedline->auth_method == uaCert) { - ereport(LOG, - (errmsg("no match in usermap \"%s\" for user \"%s\" authenticated as \"%s\"", - usermap_name, pg_role, auth_user))); + /* + * For auth method cert, client certificate validation is mandatory, and it implies + * the level of verify-full. + */ + parsedline->clientcert = clientCertFull; } - return found_entry ? STATUS_OK : STATUS_ERROR; -} + return parsedline; +} /* - * Read the ident config file and create a List of IdentLine records for - * the contents. + * Tokenize the given file. * - * This works the same as load_hba(), but for the user config file. + * The output is a list of TokenizedAuthLine 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 TokenizedAuthLine 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). */ -bool -load_ident(void) +MemoryContext +tokenize_auth_file(const char *filename, FILE *file, List **tok_lines, int elevel) { - FILE *file; - List *ident_lines = NIL; - ListCell *line_cell, - *parsed_line_cell; - List *new_parsed_lines = NIL; - bool ok = true; + int line_number = 1; + StringInfoData buf; 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 = AllocSetContextCreate(CurrentMemoryContext, + "tokenize_auth_file", + ALLOCSET_SMALL_SIZES); + oldcxt = MemoryContextSwitchTo(linecxt); - linecxt = tokenize_file(IdentFileName, file, &ident_lines, LOG); - FreeFile(file); + initStringInfo(&buf); - /* 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) + *tok_lines = NIL; + + while (!feof(file) && !ferror(file)) { - TokenizedLine *tok_line = (TokenizedLine *) lfirst(line_cell); + char *lineptr; + List *current_line = NIL; + char *err_msg = NULL; + int last_backslash_buflen = 0; + int continuations = 0; - /* don't parse lines that already have errors */ - if (tok_line->err_msg != NULL) - { - ok = false; - continue; - } + /* Collect the next input line, handling backslash continuations */ + resetStringInfo(&buf); - if ((newline = parse_ident_line(tok_line)) == NULL) + while (pg_get_line_append(file, &buf, NULL)) { - /* Parse error; remember there's trouble */ - ok = false; + /* Strip trailing newline, including \r in case we're on Windows */ + buf.len = pg_strip_crlf(buf.data); /* - * 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. + * 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. */ - continue; - } - - new_parsed_lines = lappend(new_parsed_lines, newline); - } - - /* Free tokenizer memory */ - MemoryContextDelete(linecxt); - MemoryContextSwitchTo(oldcxt); + 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; + } - 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); + /* Nope, so we have the whole line */ + break; } - 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) + if (ferror(file)) { - newline = (IdentLine *) lfirst(parsed_line_cell); - if (newline->ident_user[0] == '/') - pg_regfree(&newline->re); - } - } - if (parsed_ident_context != NULL) - MemoryContextDelete(parsed_ident_context); + /* I/O error! */ + int save_errno = errno; - parsed_ident_context = ident_context; - parsed_ident_lines = new_parsed_lines; + 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; + } - return true; -} + /* 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 TokenizedAuthLine list unless it's boring + * */ + if (current_line != NIL || err_msg != NULL) + { + TokenizedAuthLine *tok_line; -/* - * 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); -} + tok_line = (TokenizedAuthLine *) palloc(sizeof(TokenizedAuthLine)); + 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; + } -/* - * 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"); + MemoryContextSwitchTo(oldcxt); - return UserAuthName[auth_method]; + return linecxt; } diff --git a/src/backend/utils/adt/Makefile b/src/backend/utils/adt/Makefile index 41b486bcef..7c722ea2ce 100644 --- a/src/backend/utils/adt/Makefile +++ b/src/backend/utils/adt/Makefile @@ -42,6 +42,7 @@ OBJS = \ geo_ops.o \ geo_selfuncs.o \ geo_spgist.o \ + hbafuncs.o \ inet_cidr_ntop.o \ inet_net_pton.o \ int.o \ diff --git a/src/backend/utils/adt/hbafuncs.c b/src/backend/utils/adt/hbafuncs.c new file mode 100644 index 0000000000..2bcaebe99e --- /dev/null +++ b/src/backend/utils/adt/hbafuncs.c @@ -0,0 +1,452 @@ +/*------------------------------------------------------------------------- + * + * hbafuncs.c + * Support functions for authentication files SQL views. + * + * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group + * Portions Copyright (c) 1994, Regents of the University of California + * + * + * IDENTIFICATION + * src/backend/utils/adt/hbafuncs.c + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "catalog/objectaddress.h" +#include "common/ip.h" +#include "funcapi.h" +#include "libpq/hba.h" +#include "miscadmin.h" +#include "utils/array.h" +#include "utils/builtins.h" +#include "utils/guc.h" +//#include "utils/tuplestore.h" + + +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); +static ArrayType *gethba_options(HbaLine *hba); + + +/* 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_auth_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) + { + TokenizedAuthLine *tok_line = (TokenizedAuthLine *) 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); +} + +/* + * 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; +} + +/* + * 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(); +} diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 8d9f3821b1..19924dca67 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -122,6 +122,16 @@ typedef struct HbaLine char *radiusports_s; } HbaLine; +/* + * 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; + typedef struct IdentLine { int linenumber; @@ -132,6 +142,22 @@ typedef struct IdentLine regex_t re; } IdentLine; +/* + * TokenizedAuthLine 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 TokenizedAuthLine 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 TokenizedAuthLine +{ + 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 */ +} TokenizedAuthLine; + /* kluge to avoid including libpq/libpq-be.h here */ typedef struct Port hbaPort; @@ -142,6 +168,9 @@ extern void hba_getauthmethod(hbaPort *port); extern int check_usermap(const char *usermap_name, const char *pg_role, const char *auth_user, bool case_sensitive); +extern HbaLine *parse_hba_line(TokenizedAuthLine *tok_line, int elevel); extern bool pg_isblank(const char c); +extern MemoryContext tokenize_auth_file(const char *filename, FILE *file, + List **tok_lines, int elevel); #endif /* HBA_H */ diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index c6b302c7b2..f3a577652f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2694,7 +2694,7 @@ ToastTupleContext ToastedAttribute TocEntry TokenAuxData -TokenizedLine +TokenizedAuthLine TrackItem TransInvalidationInfo TransState -- 2.33.1