1
0
mirror of https://github.com/postgres/postgres.git synced 2025-04-29 13:56:47 +03:00

libpq: Handle asynchronous actions during SASL

This adds the ability for a SASL mechanism to signal PQconnectPoll()
that some arbitrary work, external to the Postgres connection, is
required for authentication to continue.  There is no consumer for
this capability as part of this commit, it is infrastructure which
is required for future work on supporting the OAUTHBEARER mechanism.

To ensure that threads are not blocked waiting for the SASL mechanism
to make long-running calls, the mechanism communicates with the top-
level client via the "altsock": a file or socket descriptor, opaque to
this layer of libpq, which is signaled when work is ready to be done
again.  The altsock temporarily replaces the regular connection
descriptor, so existing PQsocket() clients should continue to operate
correctly using their existing polling implementations.

For a mechanism to use this it should set an authentication callback,
conn->async_auth(), and a cleanup callback, conn->cleanup_async_auth(),
and return SASL_ASYNC during the exchange.  It should then assign
conn->altsock during the first call to async_auth().  When the cleanup
callback is called, either because authentication has succeeded or
because the connection is being dropped, the altsock must be released
and disconnected from the PGconn object.

This was extracted from the larger OAUTHBEARER patchset which has
been developed, and reviewed by many, over several years and it is
thus likely that some reviewer credit of much earlier versions has
been accidentally omitted.

Author: Jacob Champion <jacob.champion@enterprisedb.com>
Reviewed-by: Daniel Gustafsson <daniel@yesql.se>
Reviewed-by: Peter Eisentraut <peter@eisentraut.org>
Reviewed-by: Antonin Houska <ah@cybertec.at>
Discussion: https://postgr.es/m/CAOYmi+kJqzo6XsR9TEhvVfeVNQ-TyFM5LATypm9yoQVYk=4Wrw@mail.gmail.com
This commit is contained in:
Daniel Gustafsson 2025-02-06 22:19:21 +01:00
parent 44ec095751
commit a99a32e43e
8 changed files with 227 additions and 49 deletions

View File

@ -30,6 +30,7 @@ typedef enum
SASL_COMPLETE = 0, SASL_COMPLETE = 0,
SASL_FAILED, SASL_FAILED,
SASL_CONTINUE, SASL_CONTINUE,
SASL_ASYNC,
} SASLStatus; } SASLStatus;
/* /*
@ -77,6 +78,8 @@ typedef struct pg_fe_sasl_mech
* *
* state: The opaque mechanism state returned by init() * state: The opaque mechanism state returned by init()
* *
* final: true if the server has sent a final exchange outcome
*
* input: The challenge data sent by the server, or NULL when * input: The challenge data sent by the server, or NULL when
* generating a client-first initial response (that is, when * generating a client-first initial response (that is, when
* the server expects the client to send a message to start * the server expects the client to send a message to start
@ -101,12 +104,18 @@ typedef struct pg_fe_sasl_mech
* *
* SASL_CONTINUE: The output buffer is filled with a client response. * SASL_CONTINUE: The output buffer is filled with a client response.
* Additional server challenge is expected * Additional server challenge is expected
* SASL_ASYNC: Some asynchronous processing external to the
* connection needs to be done before a response can be
* generated. The mechanism is responsible for setting up
* conn->async_auth/cleanup_async_auth appropriately
* before returning.
* SASL_COMPLETE: The SASL exchange has completed successfully. * SASL_COMPLETE: The SASL exchange has completed successfully.
* SASL_FAILED: The exchange has failed and the connection should be * SASL_FAILED: The exchange has failed and the connection should be
* dropped. * dropped.
*-------- *--------
*/ */
SASLStatus (*exchange) (void *state, char *input, int inputlen, SASLStatus (*exchange) (void *state, bool final,
char *input, int inputlen,
char **output, int *outputlen); char **output, int *outputlen);
/*-------- /*--------

View File

@ -24,7 +24,8 @@
/* The exported SCRAM callback mechanism. */ /* The exported SCRAM callback mechanism. */
static void *scram_init(PGconn *conn, const char *password, static void *scram_init(PGconn *conn, const char *password,
const char *sasl_mechanism); const char *sasl_mechanism);
static SASLStatus scram_exchange(void *opaq, char *input, int inputlen, static SASLStatus scram_exchange(void *opaq, bool final,
char *input, int inputlen,
char **output, int *outputlen); char **output, int *outputlen);
static bool scram_channel_bound(void *opaq); static bool scram_channel_bound(void *opaq);
static void scram_free(void *opaq); static void scram_free(void *opaq);
@ -205,7 +206,8 @@ scram_free(void *opaq)
* Exchange a SCRAM message with backend. * Exchange a SCRAM message with backend.
*/ */
static SASLStatus static SASLStatus
scram_exchange(void *opaq, char *input, int inputlen, scram_exchange(void *opaq, bool final,
char *input, int inputlen,
char **output, int *outputlen) char **output, int *outputlen)
{ {
fe_scram_state *state = (fe_scram_state *) opaq; fe_scram_state *state = (fe_scram_state *) opaq;

View File

@ -430,7 +430,7 @@ pg_SSPI_startup(PGconn *conn, int use_negotiate, int payloadlen)
* Initialize SASL authentication exchange. * Initialize SASL authentication exchange.
*/ */
static int static int
pg_SASL_init(PGconn *conn, int payloadlen) pg_SASL_init(PGconn *conn, int payloadlen, bool *async)
{ {
char *initialresponse = NULL; char *initialresponse = NULL;
int initialresponselen; int initialresponselen;
@ -448,7 +448,7 @@ pg_SASL_init(PGconn *conn, int payloadlen)
goto error; goto error;
} }
if (conn->sasl_state) if (conn->sasl_state && !conn->async_auth)
{ {
libpq_append_conn_error(conn, "duplicate SASL authentication request"); libpq_append_conn_error(conn, "duplicate SASL authentication request");
goto error; goto error;
@ -607,26 +607,54 @@ pg_SASL_init(PGconn *conn, int payloadlen)
Assert(conn->sasl); Assert(conn->sasl);
/*
* Initialize the SASL state information with all the information gathered
* during the initial exchange.
*
* Note: Only tls-unique is supported for the moment.
*/
conn->sasl_state = conn->sasl->init(conn,
password,
selected_mechanism);
if (!conn->sasl_state) if (!conn->sasl_state)
goto oom_error; {
/*
* Initialize the SASL state information with all the information
* gathered during the initial exchange.
*
* Note: Only tls-unique is supported for the moment.
*/
conn->sasl_state = conn->sasl->init(conn,
password,
selected_mechanism);
if (!conn->sasl_state)
goto oom_error;
}
else
{
/*
* This is only possible if we're returning from an async loop.
* Disconnect it now.
*/
Assert(conn->async_auth);
conn->async_auth = NULL;
}
/* Get the mechanism-specific Initial Client Response, if any */ /* Get the mechanism-specific Initial Client Response, if any */
status = conn->sasl->exchange(conn->sasl_state, status = conn->sasl->exchange(conn->sasl_state, false,
NULL, -1, NULL, -1,
&initialresponse, &initialresponselen); &initialresponse, &initialresponselen);
if (status == SASL_FAILED) if (status == SASL_FAILED)
goto error; goto error;
if (status == SASL_ASYNC)
{
/*
* The mechanism should have set up the necessary callbacks; all we
* need to do is signal the caller.
*
* In non-assertion builds, this postcondition is enforced at time of
* use in PQconnectPoll().
*/
Assert(conn->async_auth);
Assert(conn->cleanup_async_auth);
*async = true;
return STATUS_OK;
}
/* /*
* Build a SASLInitialResponse message, and send it. * Build a SASLInitialResponse message, and send it.
*/ */
@ -671,7 +699,7 @@ oom_error:
* the protocol. * the protocol.
*/ */
static int static int
pg_SASL_continue(PGconn *conn, int payloadlen, bool final) pg_SASL_continue(PGconn *conn, int payloadlen, bool final, bool *async)
{ {
char *output; char *output;
int outputlen; int outputlen;
@ -701,11 +729,25 @@ pg_SASL_continue(PGconn *conn, int payloadlen, bool final)
/* For safety and convenience, ensure the buffer is NULL-terminated. */ /* For safety and convenience, ensure the buffer is NULL-terminated. */
challenge[payloadlen] = '\0'; challenge[payloadlen] = '\0';
status = conn->sasl->exchange(conn->sasl_state, status = conn->sasl->exchange(conn->sasl_state, final,
challenge, payloadlen, challenge, payloadlen,
&output, &outputlen); &output, &outputlen);
free(challenge); /* don't need the input anymore */ free(challenge); /* don't need the input anymore */
if (status == SASL_ASYNC)
{
/*
* The mechanism should have set up the necessary callbacks; all we
* need to do is signal the caller.
*/
*async = true;
/*
* The mechanism may optionally generate some output to send before
* switching over to async auth, so continue onwards.
*/
}
if (final && status == SASL_CONTINUE) if (final && status == SASL_CONTINUE)
{ {
if (outputlen != 0) if (outputlen != 0)
@ -1013,12 +1055,18 @@ check_expected_areq(AuthRequest areq, PGconn *conn)
* it. We are responsible for reading any remaining extra data, specific * it. We are responsible for reading any remaining extra data, specific
* to the authentication method. 'payloadlen' is the remaining length in * to the authentication method. 'payloadlen' is the remaining length in
* the message. * the message.
*
* If *async is set to true on return, the client doesn't yet have enough
* information to respond, and the caller must temporarily switch to
* conn->async_auth() to continue driving the exchange.
*/ */
int int
pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn) pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn, bool *async)
{ {
int oldmsglen; int oldmsglen;
*async = false;
if (!check_expected_areq(areq, conn)) if (!check_expected_areq(areq, conn))
return STATUS_ERROR; return STATUS_ERROR;
@ -1176,7 +1224,7 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
* The request contains the name (as assigned by IANA) of the * The request contains the name (as assigned by IANA) of the
* authentication mechanism. * authentication mechanism.
*/ */
if (pg_SASL_init(conn, payloadlen) != STATUS_OK) if (pg_SASL_init(conn, payloadlen, async) != STATUS_OK)
{ {
/* pg_SASL_init already set the error message */ /* pg_SASL_init already set the error message */
return STATUS_ERROR; return STATUS_ERROR;
@ -1185,23 +1233,33 @@ pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn)
case AUTH_REQ_SASL_CONT: case AUTH_REQ_SASL_CONT:
case AUTH_REQ_SASL_FIN: case AUTH_REQ_SASL_FIN:
if (conn->sasl_state == NULL)
{ {
appendPQExpBufferStr(&conn->errorMessage, bool final = false;
"fe_sendauth: invalid authentication request from server: AUTH_REQ_SASL_CONT without AUTH_REQ_SASL\n");
return STATUS_ERROR; if (conn->sasl_state == NULL)
} {
oldmsglen = conn->errorMessage.len;
if (pg_SASL_continue(conn, payloadlen,
(areq == AUTH_REQ_SASL_FIN)) != STATUS_OK)
{
/* Use this message if pg_SASL_continue didn't supply one */
if (conn->errorMessage.len == oldmsglen)
appendPQExpBufferStr(&conn->errorMessage, appendPQExpBufferStr(&conn->errorMessage,
"fe_sendauth: error in SASL authentication\n"); "fe_sendauth: invalid authentication request from server: AUTH_REQ_SASL_CONT without AUTH_REQ_SASL\n");
return STATUS_ERROR; return STATUS_ERROR;
}
oldmsglen = conn->errorMessage.len;
if (areq == AUTH_REQ_SASL_FIN)
final = true;
if (pg_SASL_continue(conn, payloadlen, final, async) != STATUS_OK)
{
/*
* Append a generic error message unless pg_SASL_continue
* did set a more specific one already.
*/
if (conn->errorMessage.len == oldmsglen)
appendPQExpBufferStr(&conn->errorMessage,
"fe_sendauth: error in SASL authentication\n");
return STATUS_ERROR;
}
break;
} }
break;
default: default:
libpq_append_conn_error(conn, "authentication method %u not supported", areq); libpq_append_conn_error(conn, "authentication method %u not supported", areq);

View File

@ -19,7 +19,8 @@
/* Prototypes for functions in fe-auth.c */ /* Prototypes for functions in fe-auth.c */
extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn); extern int pg_fe_sendauth(AuthRequest areq, int payloadlen, PGconn *conn,
bool *async);
extern char *pg_fe_getusername(uid_t user_id, PQExpBuffer errorMessage); extern char *pg_fe_getusername(uid_t user_id, PQExpBuffer errorMessage);
extern char *pg_fe_getauthname(PQExpBuffer errorMessage); extern char *pg_fe_getauthname(PQExpBuffer errorMessage);

View File

@ -507,6 +507,19 @@ pqDropConnection(PGconn *conn, bool flushInput)
conn->cmd_queue_recycle = NULL; conn->cmd_queue_recycle = NULL;
/* Free authentication/encryption state */ /* Free authentication/encryption state */
if (conn->cleanup_async_auth)
{
/*
* Any in-progress async authentication should be torn down first so
* that cleanup_async_auth() can depend on the other authentication
* state if necessary.
*/
conn->cleanup_async_auth(conn);
conn->cleanup_async_auth = NULL;
}
conn->async_auth = NULL;
/* cleanup_async_auth() should have done this, but make sure */
conn->altsock = PGINVALID_SOCKET;
#ifdef ENABLE_GSS #ifdef ENABLE_GSS
{ {
OM_uint32 min_s; OM_uint32 min_s;
@ -2853,6 +2866,7 @@ PQconnectPoll(PGconn *conn)
case CONNECTION_NEEDED: case CONNECTION_NEEDED:
case CONNECTION_GSS_STARTUP: case CONNECTION_GSS_STARTUP:
case CONNECTION_CHECK_TARGET: case CONNECTION_CHECK_TARGET:
case CONNECTION_AUTHENTICATING:
break; break;
default: default:
@ -3888,6 +3902,7 @@ keep_going: /* We will come back to here until there is
int avail; int avail;
AuthRequest areq; AuthRequest areq;
int res; int res;
bool async;
/* /*
* Scan the message from current point (note that if we find * Scan the message from current point (note that if we find
@ -4076,7 +4091,17 @@ keep_going: /* We will come back to here until there is
* Note that conn->pghost must be non-NULL if we are going to * Note that conn->pghost must be non-NULL if we are going to
* avoid the Kerberos code doing a hostname look-up. * avoid the Kerberos code doing a hostname look-up.
*/ */
res = pg_fe_sendauth(areq, msgLength, conn); res = pg_fe_sendauth(areq, msgLength, conn, &async);
if (async && (res == STATUS_OK))
{
/*
* We'll come back later once we're ready to respond.
* Don't consume the request yet.
*/
conn->status = CONNECTION_AUTHENTICATING;
goto keep_going;
}
/* /*
* OK, we have processed the message; mark data consumed. We * OK, we have processed the message; mark data consumed. We
@ -4113,6 +4138,69 @@ keep_going: /* We will come back to here until there is
goto keep_going; goto keep_going;
} }
case CONNECTION_AUTHENTICATING:
{
PostgresPollingStatusType status;
if (!conn->async_auth || !conn->cleanup_async_auth)
{
/* programmer error; should not happen */
libpq_append_conn_error(conn,
"internal error: async authentication has no handler");
goto error_return;
}
/* Drive some external authentication work. */
status = conn->async_auth(conn);
if (status == PGRES_POLLING_FAILED)
goto error_return;
if (status == PGRES_POLLING_OK)
{
/* Done. Tear down the async implementation. */
conn->cleanup_async_auth(conn);
conn->cleanup_async_auth = NULL;
/*
* Cleanup must unset altsock, both as an indication that
* it's been released, and to stop pqSocketCheck from
* looking at the wrong socket after async auth is done.
*/
if (conn->altsock != PGINVALID_SOCKET)
{
Assert(false);
libpq_append_conn_error(conn,
"internal error: async cleanup did not release polling socket");
goto error_return;
}
/*
* Reenter the authentication exchange with the server. We
* didn't consume the message that started external
* authentication, so it'll be reprocessed as if we just
* received it.
*/
conn->status = CONNECTION_AWAITING_RESPONSE;
goto keep_going;
}
/*
* Caller needs to poll some more. conn->async_auth() should
* have assigned an altsock to poll on.
*/
if (conn->altsock == PGINVALID_SOCKET)
{
Assert(false);
libpq_append_conn_error(conn,
"internal error: async authentication did not set a socket for polling");
goto error_return;
}
return status;
}
case CONNECTION_AUTH_OK: case CONNECTION_AUTH_OK:
{ {
/* /*
@ -4794,6 +4882,7 @@ pqMakeEmptyPGconn(void)
conn->verbosity = PQERRORS_DEFAULT; conn->verbosity = PQERRORS_DEFAULT;
conn->show_context = PQSHOW_CONTEXT_ERRORS; conn->show_context = PQSHOW_CONTEXT_ERRORS;
conn->sock = PGINVALID_SOCKET; conn->sock = PGINVALID_SOCKET;
conn->altsock = PGINVALID_SOCKET;
conn->Pfdebug = NULL; conn->Pfdebug = NULL;
/* /*
@ -7445,6 +7534,8 @@ PQsocket(const PGconn *conn)
{ {
if (!conn) if (!conn)
return -1; return -1;
if (conn->altsock != PGINVALID_SOCKET)
return conn->altsock;
return (conn->sock != PGINVALID_SOCKET) ? conn->sock : -1; return (conn->sock != PGINVALID_SOCKET) ? conn->sock : -1;
} }

View File

@ -1049,34 +1049,43 @@ pqWriteReady(PGconn *conn)
* or both. Returns >0 if one or more conditions are met, 0 if it timed * or both. Returns >0 if one or more conditions are met, 0 if it timed
* out, -1 if an error occurred. * out, -1 if an error occurred.
* *
* If SSL is in use, the SSL buffer is checked prior to checking the socket * If an altsock is set for asynchronous authentication, that will be used in
* for read data directly. * preference to the "server" socket. Otherwise, if SSL is in use, the SSL
* buffer is checked prior to checking the socket for read data directly.
*/ */
static int static int
pqSocketCheck(PGconn *conn, int forRead, int forWrite, pg_usec_time_t end_time) pqSocketCheck(PGconn *conn, int forRead, int forWrite, pg_usec_time_t end_time)
{ {
int result; int result;
pgsocket sock;
if (!conn) if (!conn)
return -1; return -1;
if (conn->sock == PGINVALID_SOCKET)
if (conn->altsock != PGINVALID_SOCKET)
sock = conn->altsock;
else
{ {
libpq_append_conn_error(conn, "invalid socket"); sock = conn->sock;
return -1; if (sock == PGINVALID_SOCKET)
} {
libpq_append_conn_error(conn, "invalid socket");
return -1;
}
#ifdef USE_SSL #ifdef USE_SSL
/* Check for SSL library buffering read bytes */ /* Check for SSL library buffering read bytes */
if (forRead && conn->ssl_in_use && pgtls_read_pending(conn)) if (forRead && conn->ssl_in_use && pgtls_read_pending(conn))
{ {
/* short-circuit the select */ /* short-circuit the select */
return 1; return 1;
} }
#endif #endif
}
/* We will retry as long as we get EINTR */ /* We will retry as long as we get EINTR */
do do
result = PQsocketPoll(conn->sock, forRead, forWrite, end_time); result = PQsocketPoll(sock, forRead, forWrite, end_time);
while (result < 0 && SOCK_ERRNO == EINTR); while (result < 0 && SOCK_ERRNO == EINTR);
if (result < 0) if (result < 0)

View File

@ -103,6 +103,8 @@ typedef enum
CONNECTION_CHECK_STANDBY, /* Checking if server is in standby mode. */ CONNECTION_CHECK_STANDBY, /* Checking if server is in standby mode. */
CONNECTION_ALLOCATED, /* Waiting for connection attempt to be CONNECTION_ALLOCATED, /* Waiting for connection attempt to be
* started. */ * started. */
CONNECTION_AUTHENTICATING, /* Authentication is in progress with some
* external system. */
} ConnStatusType; } ConnStatusType;
typedef enum typedef enum

View File

@ -513,6 +513,12 @@ struct pg_conn
* know which auth response we're * know which auth response we're
* sending */ * sending */
/* Callbacks for external async authentication */
PostgresPollingStatusType (*async_auth) (PGconn *conn);
void (*cleanup_async_auth) (PGconn *conn);
pgsocket altsock; /* alternative socket for client to poll */
/* Transient state needed while establishing connection */ /* Transient state needed while establishing connection */
PGTargetServerType target_server_type; /* desired session properties */ PGTargetServerType target_server_type; /* desired session properties */
PGLoadBalanceType load_balance_type; /* desired load balancing PGLoadBalanceType load_balance_type; /* desired load balancing