diff --git a/doc/src/sgml/client-auth.sgml b/doc/src/sgml/client-auth.sgml index 53832d0..e0ae721 100644 --- a/doc/src/sgml/client-auth.sgml +++ b/doc/src/sgml/client-auth.sgml @@ -495,6 +495,18 @@ hostnossl database user + redirect + + + Redirects the connection to the alternative server specified if it + matches the requested database user name and IP address. This is + only available for Protocol Version 3.1 and beyond. + See for details. + + + + + ldap @@ -624,7 +636,7 @@ hostnossl database user non-null error fields indicate problems in the corresponding lines of the file. - + To connect to a particular database, a user must not only pass the @@ -1434,6 +1446,29 @@ omicron bryanh guest1 + + Redirect Authentication + + + redirect + + + + This authentication mechanism works by specifying an alternative server to redirect + client connections to. The alternative server endpoint is specified by a hostname and port, separated by a single comma. + + + + An example entry with redirect would look like the following: + host all all 127.0.0.1/32 redirect , + + + + Support for redirection is only available from wire protocol v3.1 upwards. + + + + LDAP Authentication diff --git a/doc/src/sgml/protocol.sgml b/doc/src/sgml/protocol.sgml index 3cec9e0..5b58270 100644 --- a/doc/src/sgml/protocol.sgml +++ b/doc/src/sgml/protocol.sgml @@ -4747,6 +4747,79 @@ GSSResponse (F) +RedirectClient (B) + + + + + + + + Byte1('M') + + + + Redirects client connection to alternative server. + + + + + + Int32 + + + + Length of message contents in bytes, including self. + + + + + + String + + + + Parameter name "server" to identify the redirect target host. + + + + + + String + + + + Specifies the target host name. + + + + + + String + + + + Parameter name "port" to identify the redirect target port. + + + + + + String + + + + Specifies the target host's port. + + + + + + + + + + NegotiateProtocolVersion (B) diff --git a/src/backend/libpq/auth.c b/src/backend/libpq/auth.c index 3014b17..a700a9b 100644 --- a/src/backend/libpq/auth.c +++ b/src/backend/libpq/auth.c @@ -206,6 +206,12 @@ static int pg_SSPI_make_upn(char *accountname, static int CheckRADIUSAuth(Port *port); static int PerformRadiusTransaction(const char *server, const char *secret, const char *portstr, const char *identifier, const char *user_name, const char *passwd); +/*---------------------------------------------------------------- + * Connection Redirection + *---------------------------------------------------------------- + */ +static int SendAlternativeServerName(Port *port, char **logdetail); + /* * Maximum accepted size of GSS and SSPI authentication tokens. @@ -598,6 +604,9 @@ ClientAuthentication(Port *port) case uaTrust: status = STATUS_OK; break; + case uaRedirect: + status = SendAlternativeServerName(port, &logdetail); + break; } if (ClientAuthentication_hook) @@ -609,6 +618,28 @@ ClientAuthentication(Port *port) auth_failed(port, status, logdetail); } +/* + * Send alternative server information packet to the frontend. + */ +static int +SendAlternativeServerName(Port *port, char **logdetail) +{ + StringInfoData buf; + + CHECK_FOR_INTERRUPTS(); + + pq_beginmessage(&buf, 'M'); + pq_sendstring(&buf, "server"); + pq_sendstring(&buf, port->hba->alternativeservername); + pq_sendstring(&buf, "port"); + pq_sendstring(&buf, port->hba->alternativeserverport); + + pq_endmessage(&buf); + pq_flush(); + proc_exit(0); + + return STATUS_OK; +} /* * Send an authentication request packet to the frontend. diff --git a/src/backend/libpq/hba.c b/src/backend/libpq/hba.c index acf625e..f3f7d6f 100644 --- a/src/backend/libpq/hba.c +++ b/src/backend/libpq/hba.c @@ -134,7 +134,8 @@ static const char *const UserAuthName[] = "ldap", "cert", "radius", - "peer" + "peer", + "redirect" }; @@ -1358,6 +1359,8 @@ parse_hba_line(TokenizedLine *tok_line, int elevel) #endif else if (strcmp(token->string, "radius") == 0) parsedline->auth_method = uaRADIUS; + else if (strcmp(token->string, "redirect") == 0) + parsedline->auth_method = uaRedirect; else { ereport(elevel, @@ -1384,6 +1387,49 @@ parse_hba_line(TokenizedLine *tok_line, int elevel) return NULL; } + if (parsedline->auth_method == uaRedirect) + { + /* Get the alternative server name and port */ + field = lnext(field); + if (!field) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("end-of-line before alternative server name"), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "end-of-line before alternative server name"; + return NULL; + } + tokens = lfirst(field); + if (tokens->length > 2) + { + ereport(elevel, + (errcode(ERRCODE_CONFIG_FILE_ERROR), + errmsg("multiple values specified for alternative server"), + errhint("Specify exactly one alternative server per line."), + errcontext("line %d of configuration file \"%s\"", + line_num, HbaFileName))); + *err_msg = "multiple values specified for alternative server"; + return NULL; + } + + tokencell = list_head(tokens); + token = lfirst(tokencell); + parsedline->alternativeservername = pstrdup(token->string); + + if (tokens->length == 2) + { + tokencell = lnext(tokencell); + token = lfirst(tokencell); + parsedline->alternativeserverport = pstrdup(token->string); + } + else + { + pg_itoa(DEF_PGPORT, parsedline->alternativeserverport); + } + } + /* * XXX: When using ident on local connections, change it to peer, for * backwards compatibility. @@ -2100,6 +2146,21 @@ check_hba(hbaPort *port) if (!check_role(port->user_name, roleid, hba->roles)) continue; + /* + * Check the protocol version to see if the client supports + * redirection + */ + if (hba->auth_method == uaRedirect && + PG_PROTOCOL_MAJOR(port->proto) < PG_PROTOCOL_MAJOR(PG_PROTOCOL_LATEST) || + (PG_PROTOCOL_MAJOR(port->proto) == PG_PROTOCOL_MAJOR(PG_PROTOCOL_LATEST) && + PG_PROTOCOL_MINOR(port->proto) < PG_PROTOCOL_MINOR(PG_PROTOCOL_LATEST))) + ereport(FATAL, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("Redirection is only supported by protocol version 3.1 and above. \ + Try connecting to %s:%s directly", + hba->alternativeservername, + hba->alternativeserverport))); + /* Found a record that matched! */ port->hba = hba; return; diff --git a/src/backend/libpq/pg_hba.conf.sample b/src/backend/libpq/pg_hba.conf.sample index c853e36..3af0972 100644 --- a/src/backend/libpq/pg_hba.conf.sample +++ b/src/backend/libpq/pg_hba.conf.sample @@ -43,7 +43,7 @@ # directly connected to. # # METHOD can be "trust", "reject", "md5", "password", "scram-sha-256", -# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius" or "cert". +# "gss", "sspi", "ident", "peer", "pam", "ldap", "radius", "cert" or "redirect". # Note that "password" sends passwords in clear text; "md5" or # "scram-sha-256" are preferred since they send encrypted passwords. # diff --git a/src/include/libpq/hba.h b/src/include/libpq/hba.h index 5f68f4c..1e4f2bd 100644 --- a/src/include/libpq/hba.h +++ b/src/include/libpq/hba.h @@ -38,8 +38,9 @@ typedef enum UserAuth uaLDAP, uaCert, uaRADIUS, - uaPeer -#define USER_AUTH_LAST uaPeer /* Must be last value of this enum */ + uaPeer, + uaRedirect +#define USER_AUTH_LAST uaRedirect /* Must be last value of this enum */ } UserAuth; typedef enum IPCompareMethod @@ -99,6 +100,8 @@ typedef struct HbaLine char *radiusidentifiers_s; List *radiusports; char *radiusports_s; + char *alternativeservername; + char *alternativeserverport; } HbaLine; typedef struct IdentLine diff --git a/src/interfaces/libpq/fe-connect.c b/src/interfaces/libpq/fe-connect.c index 77eebb0..26fa934 100644 --- a/src/interfaces/libpq/fe-connect.c +++ b/src/interfaces/libpq/fe-connect.c @@ -129,6 +129,7 @@ static int ldapServiceLookup(const char *purl, PQconninfoOption *options, #else #define DefaultSSLMode "disable" #endif +#define DefaultRedirectionLimit "2" /* ---------- * Definition of the conninfo parameters and their fallback resources. @@ -266,7 +267,7 @@ static const internalPQconninfoOption PQconninfoOptions[] = { {"scram_channel_binding", NULL, DefaultSCRAMChannelBinding, NULL, "SCRAM-Channel-Binding", "D", - 21, /* sizeof("tls-server-end-point") == 21 */ + 21, /* sizeof("tls-server-end-point") == 21 */ offsetof(struct pg_conn, scram_channel_binding)}, /* @@ -330,6 +331,11 @@ static const internalPQconninfoOption PQconninfoOptions[] = { "Target-Session-Attrs", "", 11, /* sizeof("read-write") = 11 */ offsetof(struct pg_conn, target_session_attrs)}, + {"redirect_limit", "PGREDIRECTLIMIT", + DefaultRedirectionLimit, NULL, + "Redirection-Count", "", 10, /* strlen(INT32_MAX) == 10 */ + offsetof(struct pg_conn, redirect_limit)}, + /* Terminating entry --- MUST BE LAST */ {NULL, NULL, NULL, NULL, NULL, NULL, 0} @@ -1805,11 +1811,11 @@ connectDBStart(PGconn *conn) #endif /* - * Set up to try to connect, with protocol 3.0 as the first attempt. + * Set up to try to connect, with protocol 3.1 as the first attempt. */ conn->whichhost = 0; conn->addr_cur = conn->connhost[0].addrlist; - conn->pversion = PG_PROTOCOL(3, 0); + conn->pversion = PG_PROTOCOL(3, 1); conn->send_appname = true; conn->status = CONNECTION_NEEDED; @@ -2007,6 +2013,14 @@ PQconnectPoll(PGconn *conn) int optval; PQExpBufferData savedMessage; + /* Variable declarations for redirection. */ + int originalMsgLen; /* Length in bytes of message sans msg type */ + int runningMsgLen; /* Length in bytes of message sans metadata */ + int availableMsgLen; + char *altServer = NULL; + char *altPort = NULL; + bool redirectionError = false; /* Flag used to mark exceptions */ + if (conn == NULL) return PGRES_POLLING_FAILED; @@ -2024,6 +2038,7 @@ PQconnectPoll(PGconn *conn) /* These are reading states */ case CONNECTION_AWAITING_RESPONSE: + case CONNECTION_REDIRECTION: case CONNECTION_AUTH_OK: { /* Load waiting data */ @@ -2655,6 +2670,25 @@ keep_going: /* We will come back to here until there is return PGRES_POLLING_READING; } + if (beresp == 'M') + { + conn->status = CONNECTION_REDIRECTION; + goto keep_going; + } + + /* + * If server sends protocol negotiation message, default to + * 3.0 protocol. + */ + if (beresp == 'v') + { + conn->pversion = PG_PROTOCOL(3, 0); + /* Must drop the old connection */ + pqDropConnection(conn, true); + conn->status = CONNECTION_NEEDED; + goto keep_going; + } + /* * Validate message type: we expect only an authentication * request or an error here. Anything else probably means @@ -2722,19 +2756,28 @@ keep_going: /* We will come back to here until there is appendPQExpBufferChar(&conn->errorMessage, '\n'); /* - * If we tried to open the connection in 3.0 protocol, - * fall back to 2.0 protocol. + * If we tried to open the connection in 3.1 protocol, + * fall back to 3.0 protocol. If that fails as well, fall + * back to 2.0 protocol. */ - if (PG_PROTOCOL_MAJOR(conn->pversion) >= 3) + if (PG_PROTOCOL_MAJOR(conn->pversion) >= 3 + && PG_PROTOCOL_MINOR(conn->pversion) >= 1) + { + conn->pversion = PG_PROTOCOL(3, 0); + } + else if (PG_PROTOCOL_MAJOR(conn->pversion) >= 3) { conn->pversion = PG_PROTOCOL(2, 0); - /* Must drop the old connection */ - pqDropConnection(conn, true); - conn->status = CONNECTION_NEEDED; - goto keep_going; + } + else + { + goto error_return; } - goto error_return; + /* Must drop the old connection */ + pqDropConnection(conn, true); + conn->status = CONNECTION_NEEDED; + goto keep_going; } /* @@ -3097,6 +3140,146 @@ keep_going: /* We will come back to here until there is conn->status = CONNECTION_OK; return PGRES_POLLING_OK; } + + case CONNECTION_REDIRECTION: + { + /* + * Check if the number of redirect attempts exceeds the limit. + */ + if (++conn->nRedirection > atoi(conn->redirect_limit)) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("Exceeded the maximum number of redirection attempts.")); + goto error_return; + } + + /* Mark 'M' consumed: a single byte for message type. */ + conn->inCursor = conn->inStart + 1; + + /* Obtain message length from packet. */ + if (pqGetInt(&originalMsgLen, sizeof(int32), conn)) + return PGRES_POLLING_READING; + + /* Obtain the number of bytes in payload. */ + runningMsgLen = originalMsgLen - sizeof(int32); + + /* + * Enlarge buffer if payload's size is greater than what is + * available. + */ + availableMsgLen = conn->inEnd - conn->inCursor; + if (availableMsgLen < runningMsgLen) + { + if (pqCheckInBufferSpace(conn->inCursor + (size_t) runningMsgLen, conn)) + return PGRES_POLLING_READING; + } + + PQExpBuffer buf = createPQExpBuffer(); + + while (pqGets(buf, conn) != EOF) + { + if (!strcmp(buf->data, "server")) + { + if (pqGets(buf, conn) == EOF) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("failed to obtain server value from redirection packet")); + + redirectionError = true; + break; + } + altServer = strdup(buf->data); + } + else if (!strcmp(buf->data, "port")) + { + if (pqGets(buf, conn) == EOF) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("failed to obtain port value from redirection packet")); + + redirectionError = true; + break; + } + altPort = strdup(buf->data); + } + else + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("unknown key in redirection server packet")); + redirectionError = true; + } + + if (redirectionError) + { + /* Free buffer to prevent memory leak on error. */ + destroyPQExpBuffer(buf); + + /* Free strdup'd variables. */ + if (altServer) + free(altServer); + + if (altPort) + free(altPort); + + goto error_return; + } + + /* + * Buffer length does not account for null-terminated + * strings. + */ + runningMsgLen -= buf->len + 1; + } + + /* Free buffer used for reading string params in packet. */ + destroyPQExpBuffer(buf); + + /* Check for extraneous data in packet. */ + if (conn->inCursor != conn->inStart + 1 + originalMsgLen) + { + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("Extraneous data in redirection packet from server\n")); + + /* Free strdup'd variables. */ + if (altServer) + free(altServer); + + if (altPort) + free(altPort); + + goto error_return; + } + + /* Mark incoming data consumed */ + conn->inStart = conn->inCursor; + + /* Drop existing connection. */ + pqDropConnection(conn, true); + + /* Set connection parameters. */ + if (conn->pghost) + { + free(conn->pghost); + conn->pghost = altServer; + } + + if (conn->pgport) + { + free(conn->pgport); + conn->pgport = altPort; + } + + /* connectDBStart() sets appropriate connection status. */ + if (!connectOptions2(conn) || !connectDBStart(conn)) + { + conn->status = CONNECTION_BAD; + appendPQExpBuffer(&conn->errorMessage, + libpq_gettext("Failed to establish connection to redirected database\n")); + } + + goto keep_going; + } + case CONNECTION_CHECK_WRITABLE: { const char *displayed_host; diff --git a/src/interfaces/libpq/libpq-fe.h b/src/interfaces/libpq/libpq-fe.h index ed9c806..38616d2 100644 --- a/src/interfaces/libpq/libpq-fe.h +++ b/src/interfaces/libpq/libpq-fe.h @@ -65,8 +65,10 @@ typedef enum CONNECTION_NEEDED, /* Internal state: connect() needed */ CONNECTION_CHECK_WRITABLE, /* Check if we could make a writable * connection. */ - CONNECTION_CONSUME /* Wait for any pending message and consume + CONNECTION_CONSUME, /* Wait for any pending message and consume * them. */ + CONNECTION_REDIRECTION /* Redirecting the connection to the + * alternative server specified in pg_hba.conf */ } ConnStatusType; typedef enum diff --git a/src/interfaces/libpq/libpq-int.h b/src/interfaces/libpq/libpq-int.h index eba23dc..d4edd40 100644 --- a/src/interfaces/libpq/libpq-int.h +++ b/src/interfaces/libpq/libpq-int.h @@ -349,7 +349,7 @@ struct pg_conn * retransmits */ char *keepalives_count; /* maximum number of TCP keepalive * retransmits */ - char *scram_channel_binding; /* SCRAM channel binding type */ + char *scram_channel_binding; /* SCRAM channel binding type */ char *sslmode; /* SSL mode (require,prefer,allow,disable) */ char *sslcompression; /* SSL compression (0 or 1) */ char *sslkey; /* client key filename */ @@ -495,6 +495,10 @@ struct pg_conn /* Buffer for receiving various parts of messages */ PQExpBufferData workBuffer; /* expansible string */ + + char *redirect_limit; /* Specifies the maximum number of times to + * attempt redirection. */ + int nRedirection; /* Number of redirects attempted so far */ }; /* PGcancel stores all data necessary to cancel a connection. A copy of this @@ -742,8 +746,8 @@ extern char *pgtls_get_peer_certificate_hash(PGconn *conn, size_t *len); * */ extern int pgtls_verify_peer_name_matches_certificate_guts(PGconn *conn, - int *names_examined, - char **first_name); + int *names_examined, + char **first_name); /* === miscellaneous macros === */ diff --git a/src/test/authentication/t/003_redirection.pl b/src/test/authentication/t/003_redirection.pl new file mode 100644 index 0000000..db0b78c --- /dev/null +++ b/src/test/authentication/t/003_redirection.pl @@ -0,0 +1,92 @@ +# Test redirection authentication method. +# +# This test cannot run on Windows as Postgres cannot be set up with Unix +# sockets and needs to go through SSPI. + +use strict; +use warnings; +use PostgresNode; +use TestLib; +use Test::More; +if ($windows_os) +{ + plan skip_all => "authentication tests cannot run on Windows"; +} +else +{ + plan tests => 3; +} + +# Delete pg_hba.conf from the given node, add a new entry to it +# and then execute a reload to refresh it. +sub reset_pg_hba +{ + my $node = shift; + my $hba_method = shift; + my $redirect_endpoint = shift; + + unlink($node->data_dir . '/pg_hba.conf'); + $node->append_conf('pg_hba.conf', "local all all $hba_method $redirect_endpoint"); + $node->reload; +} + +# Test access for a single role, useful to wrap all tests into one. +sub test_login +{ + my $node = shift; + my $role = shift; + my $password = shift; + my $expected_res = shift; + my $status_string = 'failed'; + + $status_string = 'success' if ($expected_res eq 0); + + $ENV{"PGPASSWORD"} = $password; + my $res = $node->psql('postgres', undef, extra_params => [ '-U', $role ]); + is($res, $expected_res, + "authentication $status_string for role $role with password $password" + ); +} + +# Initialize node1. +my $node1 = get_new_node('master'); +$node1->init; +$node1->start; +$node1->safe_psql( + 'postgres', + "CREATE ROLE pguser1 LOGIN PASSWORD 'postgres'; +"); + +# Initialize node2, the redirect target. +my $node2 = get_new_node('redirect'); +$node2->init; +$node2->start; +$node2->safe_psql( + 'postgres', + "CREATE ROLE pguser2 LOGIN PASSWORD 'postgres'; +"); + +# Host is identical as both nodes reside on the same machine +my $host = $node1->host; + +my $node1_port = $node1->port; +# 1. Test a redirected connection from node1 to itself. +# Add the redirect authentication method to the node1's pg_hba.conf to set up redirection to itself. +reset_pg_hba($node1, 'redirect', "$host,$node1_port"); +# A redirect from a node to itself should fail after PGREDIRECTLIMIT (default is 5) retries. +test_login($node1, 'pguser1', "postgres", 2); + + +my $node2_port = $node2->port; +# 2. Test a redirected connection from node1 to node2 with correct creds. +# Add the redirect authentication method to the node1's pg_hba.conf to set up redirection to node2. +reset_pg_hba($node1, 'redirect', "$host,$node2_port"); +# A redirect from a node to another should succeed, given the correct creds are used. +test_login($node1, 'pguser2', "postgres", 0); + + +# 3. Test a redirected connection from node1 to node2 with wrong creds. +# Add the redirect authentication method to the node1's pg_hba.conf to set up redirection to node2. +reset_pg_hba($node1, 'redirect', "$host,$node2_port"); +# A redirect from a node to another should fail if the wrong creds are used. +test_login($node1, 'pguser', "postgres", 2);