since-v11.diff.txt
text/plain
Filename: since-v11.diff.txt
Type: text/plain
Part: 0
1: 36409a76ce = 1: 0ff053cf31 common/jsonapi: support FRONTEND clients
2: 1356b729db ! 2: 26a341e1de libpq: add OAUTHBEARER SASL mechanism
@@ Commit message
Visit https://oauth.example.org/login and enter the code: FPQ2-M4BG
The OAuth issuer must support device authorization. No other OAuth flows
- are currently implemented.
+ are currently implemented (but clients may provide their own flows; see
+ below).
The client implementation requires either libcurl or libiddawc and their
development headers. Pass `curl` or `iddawc` to --with-oauth/-Doauth
@@ Commit message
Thomas Munro wrote the kqueue() implementation for oauth-curl; thanks!
+ = PQauthDataHook =
+
+ Clients may override two pieces of OAuth handling using the new
+ PQsetAuthDataHook():
+
+ - PQAUTHDATA_PROMPT_OAUTH_DEVICE: replaces the default user prompt to
+ standard error when using the builtin device authorization flow
+
+ - PQAUTHDATA_OAUTH_BEARER_TOKEN: replaces the entire OAuth flow with a
+ custom asynchronous implementation
+
+ In general, a hook implementation should examine the incoming `type` to
+ decide whether or not to handle a specific piece of authdata; if not, it
+ should delegate to the previous hook in the chain (retrievable via
+ PQgetAuthDataHook()). Otherwise, it should return an integer > 0 and
+ follow the authdata-specific instructions. Returning an integer < 0
+ signals an error condition and abandons the connection attempt.
+
+ == PQAUTHDATA_PROMPT_OAUTH_DEVICE ==
+
+ The hook should display the device prompt (URL + code) using whatever
+ method it prefers.
+
+ == PQAUTHDATA_OAUTH_BEARER_TOKEN ==
+
+ The hook should either directly return a Bearer token for the current
+ user/issuer/scope combination, if one is available without blocking, or
+ else set up an asynchronous callback to retrieve one. See the
+ documentation for PQoauthBearerRequest.
+
Several TODOs:
- don't retry forever if the server won't accept our token
- perform several sanity checks on the OAuth issuer's responses
- handle cases where the client has been set up with an issuer and
scope, but the Postgres server wants to use something different
- improve error debuggability during the OAuth handshake
- - migrate JSON parsing to the new JSON_SEM_ACTION_FAILED API convention
- fix libcurl initialization thread-safety
- harden the libcurl flow implementation
- - figure out how to report the user code and URL without the notice
- processor
+ - figure out pgsocket/int difference on Windows
- ...and more.
## configure ##
@@ src/interfaces/libpq/Makefile: endif
SHLIB_LINK += $(filter -lcrypt -ldes -lcom_err -lcrypto -lk5crypto -lkrb5 -lgssapi32 -lssl -lsocket -lnsl -lresolv -lintl -lm $(PTHREAD_LIBS), $(LIBS)) $(LDAP_LIBS_FE)
endif
+ ## src/interfaces/libpq/exports.txt ##
+@@ src/interfaces/libpq/exports.txt: PQclosePrepared 188
+ PQclosePortal 189
+ PQsendClosePrepared 190
+ PQsendClosePortal 191
++PQsetAuthDataHook 192
++PQgetAuthDataHook 193
++PQdefaultAuthDataHook 194
+
## src/interfaces/libpq/fe-auth-oauth-curl.c (new) ##
@@
+/*-------------------------------------------------------------------------
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+#include <unistd.h>
+
+#include "common/jsonapi.h"
++#include "fe-auth.h"
+#include "fe-auth-oauth.h"
+#include "libpq-int.h"
+#include "mb/pg_wchar.h"
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ */
+ struct provider provider;
+ struct device_authz authz;
++
++ bool user_prompted; /* have we already sent the authz prompt? */
+};
+
+/*
-+ * Exported function to free the async_ctx, which is stored directly on the
-+ * PGconn. This is called during pqDropConnection() so that we don't leak
-+ * resources even if PQconnectPoll() never calls us back.
++ * Frees the async_ctx, which is stored directly on the PGconn. This is called
++ * during pqDropConnection() so that we don't leak resources even if
++ * PQconnectPoll() never calls us back.
+ *
+ * TODO: we should probably call this at the end of a successful authentication,
+ * too, to proactively free up resources.
+ */
-+void
-+pg_fe_free_oauth_async_ctx(PGconn *conn, void *ctx)
++static void
++free_curl_async_ctx(PGconn *conn, void *ctx)
+{
+ struct async_ctx *actx = ctx;
+
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+#endif
+
+ state->async_ctx = actx;
++ state->free_async_ctx = free_curl_async_ctx;
+
+ initPQExpBuffer(&actx->work_data);
+ initPQExpBuffer(&actx->errbuf);
@@ src/interfaces/libpq/fe-auth-oauth-curl.c (new)
+ if (!finish_token_request(actx, &tok))
+ goto error_return;
+
-+ /*
-+ * Now that we know the token endpoint isn't broken, give the user
-+ * the login instructions.
-+ *
-+ * TODO: allow client applications to override this handling.
-+ */
-+ fprintf(stderr, "Visit %s and enter the code: %s",
-+ actx->authz.verification_uri, actx->authz.user_code);
++ if (!actx->user_prompted)
++ {
++ int res;
++ PQpromptOAuthDevice prompt = {
++ .verification_uri = actx->authz.verification_uri,
++ .user_code = actx->authz.user_code,
++ /* TODO: optional fields */
++ };
++
++ /*
++ * Now that we know the token endpoint isn't broken, give the
++ * user the login instructions.
++ */
++ res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn,
++ &prompt);
++
++ if (!res)
++ {
++ fprintf(stderr, "Visit %s and enter the code: %s",
++ prompt.verification_uri, prompt.user_code);
++ }
++ else if (res < 0)
++ {
++ actx_error(actx, "device prompt failed");
++ goto error_return;
++ }
++
++ actx->user_prompted = true;
++ }
+
+ if (tok.access_token)
+ {
@@ src/interfaces/libpq/fe-auth-oauth-iddawc.c (new)
+
+ if (!user_prompted)
+ {
++ int res;
++ PQpromptOAuthDevice prompt = {
++ .verification_uri = verification_uri,
++ .user_code = user_code,
++ /* TODO: optional fields */
++ };
++
+ /*
+ * Now that we know the token endpoint isn't broken, give the user
+ * the login instructions.
+ */
-+ pqInternalNotice(&conn->noticeHooks,
-+ "Visit %s and enter the code: %s",
-+ verification_uri, user_code);
++ res = PQauthDataHook(PQAUTHDATA_PROMPT_OAUTH_DEVICE, conn,
++ &prompt);
++
++ if (!res)
++ {
++ fprintf(stderr, "Visit %s and enter the code: %s",
++ prompt.verification_uri, prompt.user_code);
++ }
++ else if (res < 0)
++ {
++ appendPQExpBufferStr(&conn->errorMessage,
++ libpq_gettext("device prompt failed\n"));
++ goto cleanup;
++ }
+
+ user_prompted = true;
+ }
@@ src/interfaces/libpq/fe-auth-oauth-iddawc.c (new)
+ /* TODO: actually make this asynchronous */
+ state->token = run_iddawc_auth_flow(conn, conn->oauth_discovery_uri);
+ return state->token ? PGRES_POLLING_OK : PGRES_POLLING_FAILED;
-+}
-+
-+void
-+pg_fe_free_oauth_async_ctx(PGconn *conn, void *ctx)
-+{
-+ /* We currently have no async_ctx, so this should not be called. */
-+ Assert(false);
+}
## src/interfaces/libpq/fe-auth-oauth.c (new) ##
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ Assert(sasl_mechanism != NULL);
+ Assert(!strcmp(sasl_mechanism, OAUTHBEARER_NAME));
+
-+ state = malloc(sizeof(*state));
++ state = calloc(1, sizeof(*state));
+ if (!state)
+ return NULL;
+
+ state->state = FE_OAUTH_INIT;
+ state->conn = conn;
-+ state->token = NULL;
-+ state->async_ctx = NULL;
+
+ return state;
+}
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+{
+ struct json_ctx *ctx = state;
+
-+ if (oauth_json_has_error(ctx))
-+ return JSON_SUCCESS; /* short-circuit */
-+
+ if (ctx->target_field)
+ {
+ Assert(ctx->nested == 1);
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ }
+
+ ++ctx->nested;
-+ return JSON_SUCCESS; /* TODO: switch all of these to JSON_SEM_ACTION_FAILED */
++ return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
+}
+
+static JsonParseErrorType
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+{
+ struct json_ctx *ctx = state;
+
-+ if (oauth_json_has_error(ctx))
-+ return JSON_SUCCESS; /* short-circuit */
-+
+ --ctx->nested;
+ return JSON_SUCCESS;
+}
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+{
+ struct json_ctx *ctx = state;
+
-+ if (oauth_json_has_error(ctx))
-+ {
-+ /* short-circuit */
-+ free(name);
-+ return JSON_SUCCESS;
-+ }
-+
+ if (ctx->nested == 1)
+ {
+ if (!strcmp(name, ERROR_STATUS_FIELD))
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+{
+ struct json_ctx *ctx = state;
+
-+ if (oauth_json_has_error(ctx))
-+ return JSON_SUCCESS; /* short-circuit */
-+
+ if (!ctx->nested)
+ {
+ ctx->errmsg = libpq_gettext("top-level element must be an object");
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ ctx->target_field_name);
+ }
+
-+ return JSON_SUCCESS;
++ return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
+}
+
+static JsonParseErrorType
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+{
+ struct json_ctx *ctx = state;
+
-+ if (oauth_json_has_error(ctx))
-+ {
-+ /* short-circuit */
-+ free(token);
-+ return JSON_SUCCESS;
-+ }
-+
+ if (!ctx->nested)
+ {
+ ctx->errmsg = libpq_gettext("top-level element must be an object");
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ }
+
+ free(token);
-+ return JSON_SUCCESS;
++ return oauth_json_has_error(ctx) ? JSON_SEM_ACTION_FAILED : JSON_SUCCESS;
+}
+
+static bool
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+
+ err = pg_parse_json(&lex, &sem);
+
-+ if (err != JSON_SUCCESS)
-+ {
-+ errmsg = json_errdetail(err, &lex);
-+ }
-+ else if (PQExpBufferDataBroken(ctx.errbuf))
-+ {
-+ errmsg = libpq_gettext("out of memory");
-+ }
-+ else if (ctx.errmsg)
++ if (err == JSON_SEM_ACTION_FAILED)
+ {
-+ errmsg = ctx.errmsg;
++ if (PQExpBufferDataBroken(ctx.errbuf))
++ errmsg = libpq_gettext("out of memory");
++ else if (ctx.errmsg)
++ errmsg = ctx.errmsg;
++ else
++ {
++ /*
++ * Developer error: one of the action callbacks didn't call
++ * oauth_json_set_error() before erroring out.
++ */
++ Assert(oauth_json_has_error(&ctx));
++ errmsg = "<unexpected empty error>";
++ }
+ }
++ else if (err != JSON_SUCCESS)
++ errmsg = json_errdetail(err, &lex);
+
+ if (errmsg)
+ appendPQExpBuffer(&conn->errorMessage,
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ return true;
+}
+
++static void
++free_request(PGconn *conn, void *vreq)
++{
++ PQoauthBearerRequest *request = vreq;
++
++ if (request->cleanup)
++ request->cleanup(conn, request);
++
++ free(request);
++}
++
++static PostgresPollingStatusType
++run_user_oauth_flow(PGconn *conn, pgsocket *altsock)
++{
++ fe_oauth_state *state = conn->sasl_state;
++ PQoauthBearerRequest *request = state->async_ctx;
++ PostgresPollingStatusType status;
++
++ if (!request->async)
++ {
++ libpq_append_conn_error(conn, "user-defined OAuth flow provided neither a token nor an async callback");
++ return PGRES_POLLING_FAILED;
++ }
++
++ status = request->async(conn, request, altsock);
++ if (status == PGRES_POLLING_FAILED)
++ {
++ libpq_append_conn_error(conn, "user-defined OAuth flow failed");
++ return status;
++ }
++ else if (status == PGRES_POLLING_OK)
++ {
++ /*
++ * We already have a token, so copy it into the state. (We can't
++ * hold onto the original string, since it may not be safe for us to
++ * free() it.)
++ */
++ PQExpBufferData token;
++
++ if (!request->token)
++ {
++ libpq_append_conn_error(conn, "user-defined OAuth flow did not provide a token");
++ return PGRES_POLLING_FAILED;
++ }
++
++ initPQExpBuffer(&token);
++ appendPQExpBuffer(&token, "Bearer %s", request->token);
++
++ if (PQExpBufferDataBroken(token))
++ {
++ libpq_append_conn_error(conn, "out of memory");
++ return PGRES_POLLING_FAILED;
++ }
++
++ state->token = token.data;
++ return PGRES_POLLING_OK;
++ }
++
++ /* TODO: what if no altsock was set? */
++ return status;
++}
++
++static bool
++setup_token_request(PGconn *conn, fe_oauth_state *state)
++{
++ int res;
++ PQoauthBearerRequest request = {
++ .openid_configuration = conn->oauth_discovery_uri,
++ .scope = conn->oauth_scope,
++ };
++
++ Assert(request.openid_configuration);
++
++ /* The client may have overridden the OAuth flow. */
++ res = PQauthDataHook(PQAUTHDATA_OAUTH_BEARER_TOKEN, conn, &request);
++ if (res > 0)
++ {
++ PQoauthBearerRequest *request_copy;
++
++ if (request.token)
++ {
++ /*
++ * We already have a token, so copy it into the state. (We can't
++ * hold onto the original string, since it may not be safe for us to
++ * free() it.)
++ */
++ PQExpBufferData token;
++
++ initPQExpBuffer(&token);
++ appendPQExpBuffer(&token, "Bearer %s", request.token);
++
++ if (PQExpBufferDataBroken(token))
++ {
++ libpq_append_conn_error(conn, "out of memory");
++ goto fail;
++ }
++
++ state->token = token.data;
++
++ /* short-circuit */
++ if (request.cleanup)
++ request.cleanup(conn, &request);
++ return true;
++ }
++
++ request_copy = malloc(sizeof(*request_copy));
++ if (!request_copy)
++ {
++ libpq_append_conn_error(conn, "out of memory");
++ goto fail;
++ }
++
++ memcpy(request_copy, &request, sizeof(request));
++
++ conn->async_auth = run_user_oauth_flow;
++ state->async_ctx = request_copy;
++ state->free_async_ctx = free_request;
++ }
++ else if (res < 0)
++ {
++ libpq_append_conn_error(conn, "user-defined OAuth flow failed");
++ goto fail;
++ }
++ else
++ {
++ /* Use our built-in OAuth flow. */
++ conn->async_auth = pg_fe_run_oauth_flow;
++ }
++
++ return true;
++
++fail:
++ if (request.cleanup)
++ request.cleanup(conn, &request);
++ return false;
++}
++
+static bool
+derive_discovery_uri(PGconn *conn)
+{
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+ }
+
+ /*
-+ * At this point we have to hand the connection over to our
-+ * OAuth implementation. This involves a number of HTTP
-+ * connections and timed waits, so we escape the synchronous
-+ * auth processing and tell PQconnectPoll to transfer control to
-+ * our async implementation.
++ * Decide whether we're using a user-provided OAuth flow, or the
++ * one we have built in.
+ */
-+ conn->async_auth = pg_fe_run_oauth_flow;
-+ state->state = FE_OAUTH_REQUESTING_TOKEN;
-+ return SASL_ASYNC;
-+ }
++ if (!setup_token_request(conn, state))
++ return SASL_FAILED;
+
-+ /*
-+ * If we don't have a discovery URI to be able to request a token,
-+ * we ask the server for one explicitly with an empty token. This
-+ * doesn't require any asynchronous work.
-+ */
-+ state->token = strdup("");
-+ if (!state->token)
++ if (state->token)
++ {
++ /*
++ * A really smart user implementation may have already given
++ * us the token (e.g. if there was an unexpired copy already
++ * cached). In that case, we can just fall through.
++ */
++ }
++ else
++ {
++ /*
++ * Otherwise, we have to hand the connection over to our
++ * OAuth implementation. This involves a number of HTTP
++ * connections and timed waits, so we escape the synchronous
++ * auth processing and tell PQconnectPoll to transfer
++ * control to our async implementation.
++ */
++ Assert(conn->async_auth); /* should have been set already */
++ state->state = FE_OAUTH_REQUESTING_TOKEN;
++ return SASL_ASYNC;
++ }
++ }
++ else
+ {
-+ libpq_append_conn_error(conn, "out of memory");
-+ return SASL_FAILED;
++ /*
++ * If we don't have a discovery URI to be able to request a
++ * token, we ask the server for one explicitly with an empty
++ * token. This doesn't require any asynchronous work.
++ */
++ state->token = strdup("");
++ if (!state->token)
++ {
++ libpq_append_conn_error(conn, "out of memory");
++ return SASL_FAILED;
++ }
+ }
+
+ /* fall through */
@@ src/interfaces/libpq/fe-auth-oauth.c (new)
+
+ free(state->token);
+ if (state->async_ctx)
-+ pg_fe_free_oauth_async_ctx(state->conn, state->async_ctx);
++ state->free_async_ctx(state->conn, state->async_ctx);
+
+ free(state);
+}
@@ src/interfaces/libpq/fe-auth-oauth.h (new)
+
+ PGconn *conn;
+ char *token;
++
+ void *async_ctx;
++ void (*free_async_ctx) (PGconn *conn, void *ctx);
+} fe_oauth_state;
+
+extern PostgresPollingStatusType pg_fe_run_oauth_flow(PGconn *conn, pgsocket *altsock);
-+extern void pg_fe_free_oauth_async_ctx(PGconn *conn, void *ctx);
+
+#endif /* FE_AUTH_OAUTH_H */
@@ src/interfaces/libpq/fe-auth.c: pg_fe_sendauth(AuthRequest areq, int payloadlen,
{
/* Use this message if pg_SASL_continue didn't supply one */
if (conn->errorMessage.len == oldmsglen)
+@@ src/interfaces/libpq/fe-auth.c: PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user,
+
+ return crypt_pwd;
+ }
++
++PQauthDataHook_type PQauthDataHook = PQdefaultAuthDataHook;
++
++PQauthDataHook_type
++PQgetAuthDataHook(void)
++{
++ return PQauthDataHook;
++}
++
++void
++PQsetAuthDataHook(PQauthDataHook_type hook)
++{
++ PQauthDataHook = hook ? hook : PQdefaultAuthDataHook;
++}
++
++int
++PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data)
++{
++ return 0; /* handle nothing */
++}
## src/interfaces/libpq/fe-auth.h ##
@@
+ #include "libpq-int.h"
++extern PQauthDataHook_type PQauthDataHook;
++
++
/* 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,
@@ src/interfaces/libpq/fe-connect.c: keep_going: /* We will come back to here
+ case CONNECTION_AUTHENTICATING:
+ {
+ PostgresPollingStatusType status;
-+ pgsocket altsock;
++ pgsocket altsock = PGINVALID_SOCKET;
+
+ if (!conn->async_auth)
+ {
@@ src/interfaces/libpq/fe-misc.c: pqSocketCheck(PGconn *conn, int forRead, int for
if (result < 0)
## src/interfaces/libpq/libpq-fe.h ##
+@@ src/interfaces/libpq/libpq-fe.h: extern "C"
+ #define LIBPQ_HAS_TRACE_FLAGS 1
+ /* Indicates that PQsslAttribute(NULL, "library") is useful */
+ #define LIBPQ_HAS_SSL_LIBRARY_DETECTION 1
++/* Indicates presence of the PQAUTHDATA_PROMPT_OAUTH_DEVICE authdata hook */
++#define LIBPQ_HAS_PROMPT_OAUTH_DEVICE 1
+
+ /*
+ * Option flags for PQcopyResult
@@ src/interfaces/libpq/libpq-fe.h: typedef enum
CONNECTION_CONSUME, /* Consuming any extra messages. */
CONNECTION_GSS_STARTUP, /* Negotiating GSSAPI. */
@@ src/interfaces/libpq/libpq-fe.h: typedef enum
} ConnStatusType;
typedef enum
+@@ src/interfaces/libpq/libpq-fe.h: typedef enum
+ PQ_PIPELINE_ABORTED
+ } PGpipelineStatus;
+
++typedef enum
++{
++ PQAUTHDATA_PROMPT_OAUTH_DEVICE, /* user must visit a device-authorization URL */
++ PQAUTHDATA_OAUTH_BEARER_TOKEN, /* server requests an OAuth Bearer token */
++} PGAuthData;
++
+ /* PGconn encapsulates a connection to the backend.
+ * The contents of this struct are not supposed to be known to applications.
+ */
+@@ src/interfaces/libpq/libpq-fe.h: extern int PQenv2encoding(void);
+
+ /* === in fe-auth.c === */
+
++typedef struct _PQpromptOAuthDevice
++{
++ const char *verification_uri; /* verification URI to visit */
++ const char *user_code; /* user code to enter */
++} PQpromptOAuthDevice;
++
++typedef struct _PQoauthBearerRequest
++{
++ /* Hook inputs (constant across all calls) */
++ const char * const openid_configuration; /* OIDC discovery URI */
++ const char * const scope; /* required scope(s), or NULL */
++
++ /* Hook outputs */
++
++ /*
++ * Callback implementing a custom asynchronous OAuth flow.
++ *
++ * The callback may return
++ * - PGRES_POLLING_READING/WRITING, to indicate that a file descriptor has
++ * been stored in *altsock and libpq should wait until it is readable or
++ * writable before calling back;
++ * - PGRES_POLLING_OK, to indicate that the flow is complete and
++ * request->token has been set; or
++ * - PGRES_POLLING_FAILED, to indicate that token retrieval has failed.
++ *
++ * This callback is optional. If the token can be obtained without blocking
++ * during the original call to the PQAUTHDATA_OAUTH_BEARER_TOKEN hook, it
++ * may be returned directly, but one of request->async or request->token
++ * must be set by the hook.
++ */
++ PostgresPollingStatusType (*async) (PGconn *conn,
++ struct _PQoauthBearerRequest *request,
++ int *altsock);
++
++ /*
++ * Callback to clean up custom allocations. A hook implementation may use
++ * this to free request->token and any resources in request->user.
++ *
++ * This is technically optional, but highly recommended, because there is no
++ * other indication as to when it is safe to free the token.
++ */
++ void (*cleanup) (PGconn *conn, struct _PQoauthBearerRequest *request);
++
++ /*
++ * The hook should set this to the Bearer token contents for the connection,
++ * once the flow is completed. The token contents must remain available to
++ * libpq until the hook's cleanup callback is called.
++ */
++ char *token;
++
++ /*
++ * Hook-defined data. libpq will not modify this pointer across calls to the
++ * async callback, so it can be used to keep track of application-specific
++ * state. Resources allocated here should be freed by the cleanup callback.
++ */
++ void *user;
++} PQoauthBearerRequest;
++
+ extern char *PQencryptPassword(const char *passwd, const char *user);
+ extern char *PQencryptPasswordConn(PGconn *conn, const char *passwd, const char *user, const char *algorithm);
+
++typedef int (*PQauthDataHook_type) (PGAuthData type, PGconn *conn, void *data);
++extern void PQsetAuthDataHook(PQauthDataHook_type hook);
++extern PQauthDataHook_type PQgetAuthDataHook(void);
++extern int PQdefaultAuthDataHook(PGAuthData type, PGconn *conn, void *data);
++
+ /* === in encnames.c === */
+
+ extern int pg_char_to_encoding(const char *name);
## src/interfaces/libpq/libpq-int.h ##
@@ src/interfaces/libpq/libpq-int.h: typedef struct pg_conn_host
3: 863a49f863 = 3: d02bc9a466 backend: add OAUTHBEARER SASL mechanism
4: 348554e5f4 ! 4: fded01d22b Add pytest suite for OAuth
@@ src/test/python/client/test_oauth.py (new)
+#
+
+import base64
++import collections
++import ctypes
+import http.server
+import json
++import logging
++import os
++import platform
+import secrets
+import sys
+import threading
+import time
++import traceback
++import types
+import urllib.parse
+from numbers import Number
+
@@ src/test/python/client/test_oauth.py (new)
+
+from .conftest import BLOCKING_TIMEOUT
+
++if platform.system() == "Darwin":
++ libpq = ctypes.cdll.LoadLibrary("libpq.5.dylib")
++else:
++ libpq = ctypes.cdll.LoadLibrary("libpq.so.5")
++
+
+def finish_handshake(conn):
+ """
@@ src/test/python/client/test_oauth.py (new)
+ thread.stop()
+
+
++#
++# PQAuthDataHook implementation, matching libpq.h
++#
++
++
++PQAUTHDATA_PROMPT_OAUTH_DEVICE = 0
++PQAUTHDATA_OAUTH_BEARER_TOKEN = 1
++
++PGRES_POLLING_FAILED = 0
++PGRES_POLLING_READING = 1
++PGRES_POLLING_WRITING = 2
++PGRES_POLLING_OK = 3
++
++
++class PQPromptOAuthDevice(ctypes.Structure):
++ _fields_ = [
++ ("verification_uri", ctypes.c_char_p),
++ ("user_code", ctypes.c_char_p),
++ ]
++
++
++class PQOAuthBearerRequest(ctypes.Structure):
++ pass
++
++
++PQOAuthBearerRequest._fields_ = [
++ ("openid_configuration", ctypes.c_char_p),
++ ("scope", ctypes.c_char_p),
++ (
++ "async_",
++ ctypes.CFUNCTYPE(
++ ctypes.c_int,
++ ctypes.c_void_p,
++ ctypes.POINTER(PQOAuthBearerRequest),
++ ctypes.POINTER(ctypes.c_int),
++ ),
++ ),
++ (
++ "cleanup",
++ ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PQOAuthBearerRequest)),
++ ),
++ ("token", ctypes.c_char_p),
++ ("user", ctypes.c_void_p),
++]
++
++
++@pytest.fixture
++def auth_data_cb():
++ """
++ Tracks calls to the libpq authdata hook. The yielded object contains a calls
++ member that records the data sent to the hook. If a test needs to perform
++ custom actions during a call, it can set the yielded object's impl callback;
++ beware that the callback takes place on a different thread.
++
++ This is done differently from the other callback implementations on purpose.
++ For the others, we can declare test-specific callbacks and have them perform
++ direct assertions on the data they receive. But that won't work for a C
++ callback, because there's no way for us to bubble up the assertion through
++ libpq. Instead, this mock-style approach is taken, where we just record the
++ calls and let the test examine them later.
++ """
++
++ class _Call:
++ pass
++
++ class _cb(object):
++ def __init__(self):
++ self.calls = []
++
++ cb = _cb()
++ cb.impl = None
++
++ # The callback will occur on a different thread, so protect the cb object.
++ cb_lock = threading.Lock()
++
++ @ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_byte, ctypes.c_void_p, ctypes.c_void_p)
++ def auth_data_cb(typ, pgconn, data):
++ handle_by_default = 0 # does an implementation have to be provided?
++
++ if typ == PQAUTHDATA_PROMPT_OAUTH_DEVICE:
++ cls = PQPromptOAuthDevice
++ handle_by_default = 1
++ elif typ == PQAUTHDATA_OAUTH_BEARER_TOKEN:
++ cls = PQOAuthBearerRequest
++ else:
++ return 0
++
++ call = _Call()
++ call.type = typ
++
++ # The lifetime of the underlying data being pointed to doesn't
++ # necessarily match the lifetime of the Python object, so we can't
++ # reference a Structure's fields after returning. Explicitly copy the
++ # contents over, field by field.
++ data = ctypes.cast(data, ctypes.POINTER(cls))
++ for name, _ in cls._fields_:
++ setattr(call, name, getattr(data.contents, name))
++
++ with cb_lock:
++ cb.calls.append(call)
++
++ if cb.impl:
++ # Pass control back to the test.
++ try:
++ return cb.impl(typ, pgconn, data.contents)
++ except Exception:
++ # This can't escape into the C stack, but we can fail the flow
++ # and hope the traceback gives us enough detail.
++ logging.error(
++ "Exception during authdata hook callback:\n"
++ + traceback.format_exc()
++ )
++ return -1
++
++ return handle_by_default
++
++ libpq.PQsetAuthDataHook(auth_data_cb)
++ try:
++ yield cb
++ finally:
++ # The callback is about to go out of scope, so make sure libpq is
++ # disconnected from it. (We wouldn't want to accidentally influence
++ # later tests anyway.)
++ libpq.PQsetAuthDataHook(None)
++
++
+@pytest.mark.parametrize("secret", [None, "", "hunter2"])
+@pytest.mark.parametrize("scope", [None, "", "openid email"])
+@pytest.mark.parametrize("retries", [0, 1])
@@ src/test/python/client/test_oauth.py (new)
+ ],
+)
+def test_oauth_with_explicit_issuer(
-+ capfd, accept, openid_provider, asynchronous, retries, scope, secret
++ accept, openid_provider, asynchronous, retries, scope, secret, auth_data_cb
+):
+ client_id = secrets.token_hex()
+
@@ src/test/python/client/test_oauth.py (new)
+ finish_handshake(conn)
+
+ if retries:
-+ # Finally, make sure that the client prompted the user with the expected
-+ # authorization URL and user code.
-+ expected = f"Visit {verification_url} and enter the code: {user_code}"
-+ _, stderr = capfd.readouterr()
-+ assert expected in stderr
++ # Finally, make sure that the client prompted the user once with the
++ # expected authorization URL and user code.
++ assert len(auth_data_cb.calls) == 2
++
++ # First call should have been for a custom flow, which we ignored.
++ assert auth_data_cb.calls[0].type == PQAUTHDATA_OAUTH_BEARER_TOKEN
++
++ # Second call is for our user prompt.
++ call = auth_data_cb.calls[1]
++ assert call.type == PQAUTHDATA_PROMPT_OAUTH_DEVICE
++ assert call.verification_uri.decode() == verification_url
++ assert call.user_code.decode() == user_code
+
+
+def expect_disconnected_handshake(sock):
@@ src/test/python/client/test_oauth.py (new)
+ finish_handshake(conn)
+
+
++@pytest.fixture
++def self_pipe():
++ """
++ Yields a pipe fd pair.
++ """
++
++ class _Pipe:
++ pass
++
++ p = _Pipe()
++ p.readfd, p.writefd = os.pipe()
++
++ try:
++ yield p
++ finally:
++ os.close(p.readfd)
++ os.close(p.writefd)
++
++
++@pytest.mark.parametrize("scope", [None, "", "openid email"])
++@pytest.mark.parametrize(
++ "retries",
++ [
++ -1, # no async callback
++ 0, # async callback immediately returns token
++ 1, # async callback waits on altsock once
++ 2, # async callback waits on altsock twice
++ ],
++)
++@pytest.mark.parametrize(
++ "asynchronous",
++ [
++ pytest.param(False, id="synchronous"),
++ pytest.param(True, id="asynchronous"),
++ ],
++)
++def test_user_defined_flow(
++ accept, auth_data_cb, self_pipe, scope, retries, asynchronous
++):
++ issuer = "http://localhost"
++ discovery_uri = issuer + "/.well-known/openid-configuration"
++ access_token = secrets.token_urlsafe()
++
++ sock, _ = accept(
++ oauth_issuer=issuer,
++ oauth_client_id="some-id",
++ oauth_scope=scope,
++ async_=asynchronous,
++ )
++
++ # Track callbacks.
++ attempts = 0
++ wakeup_called = False
++ cleanup_calls = 0
++ lock = threading.Lock()
++
++ def wakeup():
++ """Writes a byte to the wakeup pipe."""
++ nonlocal wakeup_called
++ with lock:
++ wakeup_called = True
++ os.write(self_pipe.writefd, b"\0")
++
++ def get_token(pgconn, request, p_altsock):
++ """
++ Async token callback. While attempts < retries, libpq will be instructed
++ to wait on the self_pipe. When attempts == retries, the token will be
++ set.
++
++ Note that assertions and exceptions raised here are allowed but not very
++ helpful, since they can't bubble through the libpq stack to be collected
++ by the test suite. Try not to rely too heavily on them.
++ """
++ # Make sure libpq passed our user data through.
++ assert request.user == 42
++
++ with lock:
++ nonlocal attempts, wakeup_called
++
++ if attempts:
++ # If we've already started the timer, we shouldn't get a
++ # call back before it trips.
++ assert wakeup_called, "authdata hook was called before the timer"
++
++ # Drain the wakeup byte.
++ os.read(self_pipe.readfd, 1)
++
++ if attempts < retries:
++ attempts += 1
++
++ # Wake up the client in a little bit of time.
++ wakeup_called = False
++ threading.Timer(0.1, wakeup).start()
++
++ # Tell libpq to wait on the other end of the wakeup pipe.
++ p_altsock[0] = self_pipe.readfd
++ return PGRES_POLLING_READING
++
++ # Done!
++ request.token = access_token.encode()
++ return PGRES_POLLING_OK
++
++ @ctypes.CFUNCTYPE(
++ ctypes.c_int,
++ ctypes.c_void_p,
++ ctypes.POINTER(PQOAuthBearerRequest),
++ ctypes.POINTER(ctypes.c_int),
++ )
++ def get_token_wrapper(pgconn, p_request, p_altsock):
++ """
++ Translation layer between C and Python for the async callback.
++ Assertions and exceptions will be swallowed at the boundary, so make
++ sure they don't escape here.
++ """
++ try:
++ return get_token(pgconn, p_request.contents, p_altsock)
++ except Exception:
++ logging.error("Exception during async callback:\n" + traceback.format_exc())
++ return PGRES_POLLING_FAILED
++
++ @ctypes.CFUNCTYPE(None, ctypes.c_void_p, ctypes.POINTER(PQOAuthBearerRequest))
++ def cleanup(pgconn, p_request):
++ """
++ Should be called exactly once per connection.
++ """
++ nonlocal cleanup_calls
++ with lock:
++ cleanup_calls += 1
++
++ def bearer_hook(typ, pgconn, request):
++ """
++ Implementation of the PQAuthDataHook, which either sets up an async
++ callback or returns the token directly, depending on the value of
++ retries.
++
++ As above, try not to rely too much on assertions/exceptions here.
++ """
++ assert typ == PQAUTHDATA_OAUTH_BEARER_TOKEN
++ request.cleanup = cleanup
++
++ if retries < 0:
++ # Special case: return a token immediately without a callback.
++ request.token = access_token.encode()
++ return 1
++
++ # Tell libpq to call us back.
++ request.async_ = get_token_wrapper
++ request.user = ctypes.c_void_p(42) # will be checked in the callback
++ return 1
++
++ auth_data_cb.impl = bearer_hook
++
++ # Now drive the server side.
++ with sock:
++ with pq3.wrap(sock, debug_stream=sys.stdout) as conn:
++ # Initiate a handshake, which should result in our custom callback
++ # being invoked to fetch the token.
++ initial = start_oauth_handshake(conn)
++
++ # Validate and accept the token.
++ auth = get_auth_value(initial)
++ assert auth == f"Bearer {access_token}".encode("ascii")
++
++ pq3.send(conn, pq3.types.AuthnRequest, type=pq3.authn.SASLFinal)
++ finish_handshake(conn)
++
++ # Check the data provided to the hook.
++ assert len(auth_data_cb.calls) == 1
++
++ call = auth_data_cb.calls[0]
++ assert call.type == PQAUTHDATA_OAUTH_BEARER_TOKEN
++ assert call.openid_configuration.decode() == discovery_uri
++ assert call.scope == (None if scope is None else scope.encode())
++
++ # Make sure we cleaned up after ourselves.
++ assert cleanup_calls == 1
++
++
+def alt_patterns(*patterns):
+ """
+ Just combines multiple alternative regexes into one. It's not very efficient
5: 16d3984a45 ! 5: 38a9691801 squash! Add pytest suite for OAuth
@@ src/test/python/README: To make quick smoke tests possible, slow tests have been
+ $ py.test --temp-instance=./tmp_check
## src/test/python/client/test_oauth.py ##
-@@
- import base64
- import http.server
- import json
-+import os
- import secrets
- import sys
- import threading
@@ src/test/python/client/test_oauth.py: import pq3
from .conftest import BLOCKING_TIMEOUT
@@ src/test/python/client/test_oauth.py: import pq3
+ reason="OAuth client tests require --with-oauth support",
+)
+
-
- def finish_handshake(conn):
- """
+ if platform.system() == "Darwin":
+ libpq = ctypes.cdll.LoadLibrary("libpq.5.dylib")
+ else:
## src/test/python/conftest.py ##
@@ src/test/python/conftest.py: import os