since-v5.diff.txt

text/plain

Filename: since-v5.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
 1:  a94a11d56c <  -:  ---------- Add support for custom authentication methods
 2:  973f622fea <  -:  ---------- Add sample extension to test custom auth provider hooks
 3:  b4a0ab5a4e <  -:  ---------- Add tests for test_auth_provider extension
 4:  49715c3c98 <  -:  ---------- Add support for "map" and custom auth options
 5:  2b2e8d3050 !  1:  c3698bbc3d common/jsonapi: support FRONTEND clients
    @@ src/common/jsonapi.c: parse_object(JsonLexContext *lex, JsonSemAction *sem)
      #endif
      
     @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
    - 	char	   *const end = lex->input + lex->input_length;
    - 	int			hi_surrogate = -1;
    + 		return code; \
    + 	} while (0)
      
     -	if (lex->strval != NULL)
     -		resetStringInfo(lex->strval);
    @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
      	Assert(lex->input_length > 0);
      	s = lex->token_start;
     @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
    - 						return JSON_UNICODE_ESCAPE_FORMAT;
    - 					}
    + 					else
    + 						FAIL_AT_CHAR_END(JSON_UNICODE_ESCAPE_FORMAT);
      				}
     -				if (lex->strval != NULL)
     +				if (lex->parse_strval)
    @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
     +						appendPQExpBufferChar(lex->strval, (char) ch);
      					}
      					else
    - 						return JSON_UNICODE_HIGH_ESCAPE;
    + 						FAIL_AT_CHAR_END(JSON_UNICODE_HIGH_ESCAPE);
      #endif							/* FRONTEND */
      				}
      			}
    @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
     +			else if (lex->parse_strval)
      			{
      				if (hi_surrogate != -1)
    - 					return JSON_UNICODE_LOW_SURROGATE;
    + 					FAIL_AT_CHAR_END(JSON_UNICODE_LOW_SURROGATE);
     @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
      					case '"':
      					case '\\':
    @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
     +						appendStrValChar(lex->strval, '\t');
      						break;
      					default:
    - 						/* Not a valid string escape, so signal error. */
    + 
     @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
      
      			/*
    @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
      			/*
      			 * s will be incremented at the top of the loop, so set it to just
     @@ src/common/jsonapi.c: json_lex_string(JsonLexContext *lex)
    - 	if (hi_surrogate != -1)
      		return JSON_UNICODE_LOW_SURROGATE;
    + 	}
      
     +#ifdef FRONTEND
     +	if (lex->parse_strval && PQExpBufferBroken(lex->strval))
    @@ src/common/jsonapi.c: report_parse_error(JsonParseContext ctx, JsonLexContext *l
     -}
     -
      /*
    -  * Construct a detail message for a JSON error.
    +  * Construct an (already translated) detail message for a JSON error.
       *
     - * Note that the error message generated by this routine may not be
     - * palloc'd, making it unsafe for frontend code as there is no way to
    -- * know if this can be safery pfree'd or not.
    +- * know if this can be safely pfree'd or not.
     + * The returned allocation is either static or owned by the JsonLexContext and
     + * should not be freed.
       */
    @@ src/common/jsonapi.c: report_parse_error(JsonParseContext ctx, JsonLexContext *l
      			return _("\\u0000 cannot be converted to text.");
      		case JSON_UNICODE_ESCAPE_FORMAT:
     @@ src/common/jsonapi.c: json_errdetail(JsonParseErrorType error, JsonLexContext *lex)
    - 			return _("Unicode low surrogate must follow a high surrogate.");
    + 			/* note: this case is only reachable in frontend not backend */
    + 			return _("Unicode escape values cannot be used for code point values above 007F when the encoding is not UTF8.");
    + 		case JSON_UNICODE_UNTRANSLATABLE:
    +-			/* note: this case is only reachable in backend not frontend */
    ++			/*
    ++			 * note: this case is only reachable in backend not frontend.
    ++			 * #ifdef it away so the frontend doesn't try to link against
    ++			 * backend functionality.
    ++			 */
    ++#ifndef FRONTEND
    + 			return psprintf(_("Unicode escape value could not be translated to the server's encoding %s."),
    + 							GetDatabaseEncodingName());
    ++#else
    ++			Assert(false);
    ++			break;
    ++#endif
    + 		case JSON_UNICODE_HIGH_SURROGATE:
    + 			return _("Unicode high surrogate must not follow a high surrogate.");
    + 		case JSON_UNICODE_LOW_SURROGATE:
    +@@ src/common/jsonapi.c: json_errdetail(JsonParseErrorType error, JsonLexContext *lex)
    + 			break;
      	}
      
     -	/*
    @@ src/include/common/jsonapi.h
      
     -#include "lib/stringinfo.h"
     -
    - typedef enum
    + typedef enum JsonTokenType
      {
      	JSON_TOKEN_INVALID,
    -@@ src/include/common/jsonapi.h: typedef enum
    +@@ src/include/common/jsonapi.h: typedef enum JsonParseErrorType
      	JSON_EXPECTED_OBJECT_NEXT,
      	JSON_EXPECTED_STRING,
      	JSON_INVALID_TOKEN,
    @@ src/include/common/jsonapi.h: typedef enum
      	JSON_UNICODE_CODE_POINT_ZERO,
      	JSON_UNICODE_ESCAPE_FORMAT,
      	JSON_UNICODE_HIGH_ESCAPE,
    -@@ src/include/common/jsonapi.h: typedef enum
    - 	JSON_UNICODE_LOW_SURROGATE
    +@@ src/include/common/jsonapi.h: typedef enum JsonParseErrorType
    + 	JSON_SEM_ACTION_FAILED		/* error should already be reported */
      } JsonParseErrorType;
      
     +/*
    @@ src/include/common/jsonapi.h: typedef struct JsonLexContext
     +	StrValType *errormsg;
      } JsonLexContext;
      
    - typedef void (*json_struct_action) (void *state);
    + typedef JsonParseErrorType (*json_struct_action) (void *state);
     @@ src/include/common/jsonapi.h: extern PGDLLIMPORT JsonSemAction nullSemAction;
       */
      extern JsonParseErrorType json_count_array_elements(JsonLexContext *lex,
 6:  c18d7da6cc !  2:  0cd726fd55 libpq: add OAUTHBEARER SASL mechanism
    @@ Commit message
         - 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
         - ...and more.
     
      ## configure ##
    @@ configure: fi
     +  as_fn_error $? "library 'iddawc' is required for OAuth support" "$LINENO" 5
     +fi
     +
    ++  # Check for an older spelling of i_get_openid_config
    ++  for ac_func in i_load_openid_config
    ++do :
    ++  ac_fn_c_check_func "$LINENO" "i_load_openid_config" "ac_cv_func_i_load_openid_config"
    ++if test "x$ac_cv_func_i_load_openid_config" = xyes; then :
    ++  cat >>confdefs.h <<_ACEOF
    ++#define HAVE_I_LOAD_OPENID_CONFIG 1
    ++_ACEOF
    ++
    ++fi
    ++done
    ++
     +fi
     +
      # for contrib/sepgsql
    @@ configure.ac: fi
      
     +if test "$with_oauth" = yes ; then
     +  AC_CHECK_LIB(iddawc, i_init_session, [], [AC_MSG_ERROR([library 'iddawc' is required for OAuth support])])
    ++  # Check for an older spelling of i_get_openid_config
    ++  AC_CHECK_FUNCS([i_load_openid_config])
     +fi
     +
      # for contrib/sepgsql
    @@ configure.ac: elif test "$with_uuid" = ossp ; then
         AC_CHECK_HEADERS(crtdefs.h)
      fi
     
    + ## meson.build ##
    +@@ meson.build: endif
    + 
    + 
    + 
    ++###############################################################
    ++# Library: oauth
    ++###############################################################
    ++
    ++oauthopt = get_option('oauth')
    ++if not oauthopt.disabled()
    ++  oauth = dependency('libiddawc', required: oauthopt)
    ++
    ++  if oauth.found()
    ++    cdata.set('USE_OAUTH', 1)
    ++    # Check for an older spelling of i_get_openid_config
    ++    if cc.has_function('i_load_openid_config',
    ++                       dependencies: oauth, args: test_c_args)
    ++      cdata.set('HAVE_I_LOAD_OPENID_CONFIG', 1)
    ++    endif
    ++  endif
    ++else
    ++  oauth = not_found_dep
    ++endif
    ++
    ++
    + ###############################################################
    + # Library: Tcl (for pltcl)
    + #
    +@@ meson.build: libpq_deps += [
    +   gssapi,
    +   ldap_r,
    +   libintl,
    ++  oauth,
    +   ssl,
    + ]
    + 
    +@@ meson.build: if meson.version().version_compare('>=0.57')
    +       'llvm': llvm,
    +       'lz4': lz4,
    +       'nls': libintl,
    ++      'oauth': oauth,
    +       'openssl': ssl,
    +       'pam': pam,
    +       'plperl': perl_dep,
    +
    + ## meson_options.txt ##
    +@@ meson_options.txt: option('lz4', type : 'feature', value: 'auto',
    + option('nls', type: 'feature', value: 'auto',
    +   description: 'native language support')
    + 
    ++option('oauth', type : 'feature', value: 'auto',
    ++  description: 'OAuth 2.0 support')
    ++
    + option('pam', type : 'feature', value: 'auto',
    +   description: 'build with PAM support')
    + 
    +
      ## src/Makefile.global.in ##
     @@ src/Makefile.global.in: with_ldap	= @with_ldap@
      with_libxml	= @with_libxml@
    @@ src/Makefile.global.in: with_ldap	= @with_ldap@
      with_uuid	= @with_uuid@
      with_zlib	= @with_zlib@
     
    + ## src/common/meson.build ##
    +@@ src/common/meson.build: common_sources_frontend_static += files(
    + # For the server build of pgcommon, depend on lwlocknames_h, because at least
    + # cryptohash_openssl.c, hmac_openssl.c depend on it. That's arguably a
    + # layering violation, but ...
    ++#
    ++# XXX Frontend builds need libpq's pqexpbuffer.h, so adjust the include paths
    ++# appropriately. This seems completely broken.
    + pgcommon = {}
    + pgcommon_variants = {
    +   '_srv': internal_lib_args + {
    ++    'include_directories': include_directories('.'),
    +     'sources': common_sources + [lwlocknames_h],
    +     'dependencies': [backend_common_code],
    +   },
    +   '': default_lib_args + {
    ++    'include_directories': include_directories('../interfaces/libpq', '.'),
    +     'sources': common_sources_frontend_static,
    +     'dependencies': [frontend_common_code],
    +   },
    +   '_shlib': default_lib_args + {
    +     'pic': true,
    ++    'include_directories': include_directories('../interfaces/libpq', '.'),
    +     'sources': common_sources_frontend_shlib,
    +     'dependencies': [frontend_common_code],
    +   },
    +@@ src/common/meson.build: foreach name, opts : pgcommon_variants
    +     c_args = opts.get('c_args', []) + common_cflags[cflagname]
    +     cflag_libs += static_library('libpgcommon@0@_@1@'.format(name, cflagname),
    +       c_pch: pch_c_h,
    +-      include_directories: include_directories('.'),
    +       kwargs: opts + {
    +         'sources': sources,
    +         'c_args': c_args,
    +@@ src/common/meson.build: foreach name, opts : pgcommon_variants
    +   lib = static_library('libpgcommon@0@'.format(name),
    +       link_with: cflag_libs,
    +       c_pch: pch_c_h,
    +-      include_directories: include_directories('.'),
    +       kwargs: opts + {
    +         'dependencies': opts['dependencies'] + [ssl],
    +       }
    +
      ## src/include/common/oauth-common.h (new) ##
     @@
     +/*-------------------------------------------------------------------------
    @@ src/include/common/oauth-common.h (new)
     +#endif /* OAUTH_COMMON_H */
     
      ## src/include/pg_config.h.in ##
    +@@
    + /* Define to 1 if __builtin_constant_p(x) implies "i"(x) acceptance. */
    + #undef HAVE_I_CONSTRAINT__BUILTIN_CONSTANT_P
    + 
    ++/* Define to 1 if you have the `i_load_openid_config' function. */
    ++#undef HAVE_I_LOAD_OPENID_CONFIG
    ++
    + /* Define to 1 if you have the `kqueue' function. */
    + #undef HAVE_KQUEUE
    + 
     @@
      /* Define to 1 if you have the `crypto' library (-lcrypto). */
      #undef HAVE_LIBCRYPTO
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +#include "fe-auth.h"
     +#include "mb/pg_wchar.h"
     +
    ++#ifdef HAVE_I_LOAD_OPENID_CONFIG
    ++/* Older versions of iddawc used 'load' instead of 'get' for some APIs. */
    ++#define i_get_openid_config i_load_openid_config
    ++#endif
    ++
     +/* The exported OAuth callback mechanism. */
     +static void *oauth_init(PGconn *conn, const char *password,
     +						const char *sasl_mechanism);
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +		(ctx)->errmsg = (ctx)->errbuf.data; \
     +	} while (0)
     +
    -+static void
    ++static JsonParseErrorType
     +oauth_json_object_start(void *state)
     +{
     +	struct json_ctx	   *ctx = state;
     +
     +	if (oauth_json_has_error(ctx))
    -+		return; /* short-circuit */
    ++		return JSON_SUCCESS; /* short-circuit */
     +
     +	if (ctx->target_field)
     +	{
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	}
     +
     +	++ctx->nested;
    ++	return JSON_SUCCESS; /* TODO: switch all of these to JSON_SEM_ACTION_FAILED */
     +}
     +
    -+static void
    ++static JsonParseErrorType
     +oauth_json_object_end(void *state)
     +{
     +	struct json_ctx	   *ctx = state;
     +
     +	if (oauth_json_has_error(ctx))
    -+		return; /* short-circuit */
    ++		return JSON_SUCCESS; /* short-circuit */
     +
     +	--ctx->nested;
    ++	return JSON_SUCCESS;
     +}
     +
    -+static void
    ++static JsonParseErrorType
     +oauth_json_object_field_start(void *state, char *name, bool isnull)
     +{
     +	struct json_ctx	   *ctx = state;
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	{
     +		/* short-circuit */
     +		free(name);
    -+		return;
    ++		return JSON_SUCCESS;
     +	}
     +
     +	if (ctx->nested == 1)
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	}
     +
     +	free(name);
    ++	return JSON_SUCCESS;
     +}
     +
    -+static void
    ++static JsonParseErrorType
     +oauth_json_array_start(void *state)
     +{
     +	struct json_ctx	   *ctx = state;
     +
     +	if (oauth_json_has_error(ctx))
    -+		return; /* short-circuit */
    ++		return JSON_SUCCESS; /* short-circuit */
     +
     +	if (!ctx->nested)
     +	{
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +							 libpq_gettext("field \"%s\" must be a string"),
     +							 ctx->target_field_name);
     +	}
    ++
    ++	return JSON_SUCCESS;
     +}
     +
    -+static void
    ++static JsonParseErrorType
     +oauth_json_scalar(void *state, char *token, JsonTokenType type)
     +{
     +	struct json_ctx	   *ctx = state;
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	{
     +		/* short-circuit */
     +		free(token);
    -+		return;
    ++		return JSON_SUCCESS;
     +	}
     +
     +	if (!ctx->nested)
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +			ctx->target_field = NULL;
     +			ctx->target_field_name = NULL;
     +
    -+			return; /* don't free the token we're using */
    ++			return JSON_SUCCESS; /* don't free the token we're using */
     +		}
     +
     +		oauth_json_set_error(ctx,
    @@ src/interfaces/libpq/fe-auth-oauth.c (new)
     +	}
     +
     +	free(token);
    ++	return JSON_SUCCESS;
     +}
     +
     +static bool
    @@ src/interfaces/libpq/fe-auth.c: pg_SASL_continue(PGconn *conn, int payloadlen, b
      						 &done, &success);
     
      ## src/interfaces/libpq/fe-auth.h ##
    -@@ src/interfaces/libpq/fe-auth.h: extern const pg_fe_sasl_mech pg_scram_mech;
    - extern char *pg_fe_scram_build_secret(const char *password,
    +@@ src/interfaces/libpq/fe-auth.h: extern char *pg_fe_scram_build_secret(const char *password,
    + 									  int iterations,
      									  const char **errstr);
      
     +/* Mechanisms in fe-auth-oauth.c */
    @@ src/interfaces/libpq/fe-auth.h: extern const pg_fe_sasl_mech pg_scram_mech;
     
      ## src/interfaces/libpq/fe-connect.c ##
     @@ src/interfaces/libpq/fe-connect.c: static const internalPQconninfoOption PQconninfoOptions[] = {
    - 		"Target-Session-Attrs", "", 15, /* sizeof("prefer-standby") = 15 */
    - 	offsetof(struct pg_conn, target_session_attrs)},
    + 		"Load-Balance-Hosts", "", 8,	/* sizeof("disable") = 8 */
    + 	offsetof(struct pg_conn, load_balance_hosts)},
      
     +	/* OAuth v2 */
     +	{"oauth_issuer", NULL, NULL, NULL,
    @@ src/interfaces/libpq/fe-connect.c: keep_going:						/* We will come back to here
      
      					/*
     @@ src/interfaces/libpq/fe-connect.c: freePGconn(PGconn *conn)
    - 	free(conn->outBuffer);
      	free(conn->rowBuf);
      	free(conn->target_session_attrs);
    + 	free(conn->load_balance_hosts);
     +	free(conn->oauth_issuer);
     +	free(conn->oauth_discovery_uri);
     +	free(conn->oauth_client_id);
    @@ src/interfaces/libpq/fe-connect.c: freePGconn(PGconn *conn)
     
      ## src/interfaces/libpq/libpq-int.h ##
     @@ src/interfaces/libpq/libpq-int.h: struct pg_conn
    - 	char	   *ssl_max_protocol_version;	/* maximum TLS protocol version */
    - 	char	   *target_session_attrs;	/* desired session properties */
    + 	char	   *require_auth;	/* name of the expected auth method */
    + 	char	   *load_balance_hosts; /* load balance over hosts */
      
     +	/* OAuth v2 */
     +	char	   *oauth_issuer;			/* token issuer URL */
    @@ src/interfaces/libpq/libpq-int.h: struct pg_conn
      	/* Optional file to write trace info to */
      	FILE	   *Pfdebug;
      	int			traceFlags;
    +
    + ## src/interfaces/libpq/meson.build ##
    +@@ src/interfaces/libpq/meson.build: if gssapi.found()
    +   )
    + endif
    + 
    ++if oauth.found()
    ++  libpq_sources += files('fe-auth-oauth.c')
    ++endif
    ++
    + export_file = custom_target('libpq.exports',
    +   kwargs: gen_export_kwargs,
    + )
    +
    + ## src/makefiles/meson.build ##
    +@@ src/makefiles/meson.build: pgxs_deps = {
    +   'llvm': llvm,
    +   'lz4': lz4,
    +   'nls': libintl,
    ++  'oauth': oauth,
    +   'pam': pam,
    +   'perl': perl_dep,
    +   'python': python3_dep,
 7:  f6a81f50f2 !  3:  77889eb986 backend: add OAUTHBEARER SASL mechanism
    @@ src/backend/libpq/auth.c
      #include "libpq/pqformat.h"
      #include "libpq/sasl.h"
      #include "libpq/scram.h"
    +@@
    +  */
    + static void auth_failed(Port *port, int status, const char *logdetail);
    + static char *recv_password_packet(Port *port);
    +-static void set_authn_id(Port *port, const char *id);
    + 
    + 
    + /*----------------------------------------------------------------
    +@@ src/backend/libpq/auth.c: 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);
    + 
    + 
    +-/*
    +- * Maximum accepted size of GSS and SSPI authentication tokens.
    +- * We also use this as a limit on ordinary password packet lengths.
    +- *
    +- * Kerberos tickets are usually quite small, but the TGTs issued by Windows
    +- * domain controllers include an authorization field known as the Privilege
    +- * Attribute Certificate (PAC), which contains the user's Windows permissions
    +- * (group memberships etc.). The PAC is copied into all tickets obtained on
    +- * the basis of this TGT (even those issued by Unix realms which the Windows
    +- * realm trusts), and can be several kB in size. The maximum token size
    +- * accepted by Windows systems is determined by the MaxAuthToken Windows
    +- * registry setting. Microsoft recommends that it is not set higher than
    +- * 65535 bytes, so that seems like a reasonable limit for us as well.
    +- */
    +-#define PG_MAX_AUTH_TOKEN_LENGTH	65535
    +-
    + /*----------------------------------------------------------------
    +  * Global authentication functions
    +  *----------------------------------------------------------------
     @@ src/backend/libpq/auth.c: auth_failed(Port *port, int status, const char *logdetail)
      		case uaRADIUS:
      			errstr = gettext_noop("RADIUS authentication failed for user \"%s\"");
    @@ src/backend/libpq/auth.c: auth_failed(Port *port, int status, const char *logdet
     +		case uaOAuth:
     +			errstr = gettext_noop("OAuth bearer authentication failed for user \"%s\"");
     +			break;
    - 		case uaCustom:
    - 			{
    - 				CustomAuthProvider *provider = get_provider_by_name(port->hba->custom_provider);
    + 		default:
    + 			errstr = gettext_noop("authentication failed for user \"%s\": invalid authentication method");
    + 			break;
    +@@ src/backend/libpq/auth.c: auth_failed(Port *port, int status, const char *logdetail)
    +  * lifetime of MyClientConnectionInfo, so it is safe to pass a string that is
    +  * managed by an external library.
    +  */
    +-static void
    ++void
    + set_authn_id(Port *port, const char *id)
    + {
    + 	Assert(id);
     @@ src/backend/libpq/auth.c: ClientAuthentication(Port *port)
      		case uaTrust:
      			status = STATUS_OK;
    @@ src/backend/libpq/auth.c: ClientAuthentication(Port *port)
     +		case uaOAuth:
     +			status = CheckSASLAuth(&pg_be_oauth_mech, port, NULL, NULL);
     +			break;
    - 		case uaCustom:
    - 			{
    - 				CustomAuthProvider *provider = get_provider_by_name(port->hba->custom_provider);
    + 	}
    + 
    + 	if ((status == STATUS_OK && port->hba->clientcert == clientCertFull)
     
      ## src/backend/libpq/hba.c ##
     @@ src/backend/libpq/hba.c: static const char *const UserAuthName[] =
    + 	"ldap",
      	"cert",
      	"radius",
    - 	"custom",
     -	"peer"
     +	"peer",
     +	"oauth",
      };
      
    - 
    + /*
     @@ src/backend/libpq/hba.c: parse_hba_line(TokenizedAuthLine *tok_line, int elevel)
      #endif
      	else if (strcmp(token->string, "radius") == 0)
      		parsedline->auth_method = uaRADIUS;
     +	else if (strcmp(token->string, "oauth") == 0)
     +		parsedline->auth_method = uaOAuth;
    - 	else if (strcmp(token->string, "custom") == 0)
    - 		parsedline->auth_method = uaCustom;
      	else
    + 	{
    + 		ereport(elevel,
     @@ src/backend/libpq/hba.c: parse_hba_auth_opt(char *name, char *val, HbaLine *hbaline,
    + 			hbaline->auth_method != uaPeer &&
      			hbaline->auth_method != uaGSS &&
      			hbaline->auth_method != uaSSPI &&
    - 			hbaline->auth_method != uaCert &&
    -+			hbaline->auth_method != uaOAuth &&
    - 			hbaline->auth_method != uaCustom)
    --			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, cert and custom"));
    -+			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, cert, oauth, and custom"));
    +-			hbaline->auth_method != uaCert)
    +-			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, and cert"));
    ++			hbaline->auth_method != uaCert &&
    ++			hbaline->auth_method != uaOAuth)
    ++			INVALID_AUTH_OPTION("map", gettext_noop("ident, peer, gssapi, sspi, cert, and oauth"));
      		hbaline->usermap = pstrdup(val);
      	}
      	else if (strcmp(name, "clientcert") == 0)
    @@ src/backend/libpq/hba.c: parse_hba_auth_opt(char *name, char *val, HbaLine *hbal
     +		else
     +			hbaline->oauth_skip_usermap = false;
     +	}
    - 	else if (strcmp(name, "provider") == 0)
    + 	else
      	{
    - 		REQUIRE_AUTH_OPTION(uaCustom, "provider", "custom");
    + 		ereport(elevel,
    +
    + ## src/backend/libpq/meson.build ##
    +@@
    + # Copyright (c) 2022-2023, PostgreSQL Global Development Group
    + 
    + backend_sources += files(
    ++  'auth-oauth.c',
    +   'auth-sasl.c',
    +   'auth-scram.c',
    +   'auth.c',
     
      ## src/backend/utils/misc/guc_tables.c ##
     @@
    @@ src/backend/utils/misc/guc_tables.c
      #include "libpq/auth.h"
      #include "libpq/libpq.h"
     +#include "libpq/oauth.h"
    + #include "libpq/scram.h"
    + #include "nodes/queryjumble.h"
      #include "optimizer/cost.h"
    - #include "optimizer/geqo.h"
    - #include "optimizer/optimizer.h"
     @@ src/backend/utils/misc/guc_tables.c: struct config_string ConfigureNamesString[] =
    - 		check_backtrace_functions, assign_backtrace_functions, NULL
    + 		check_io_direct, assign_io_direct, NULL
      	},
      
     +	{
     +		{"oauth_validator_command", PGC_SIGHUP, CONN_AUTH_AUTH,
     +			gettext_noop("Command to validate OAuth v2 bearer tokens."),
     +			NULL,
    -+			GUC_SUPERUSER_ONLY
    ++			GUC_SUPERUSER_ONLY | GUC_NOT_IN_SAMPLE
     +		},
     +		&oauth_validator_command,
     +		"",
    @@ src/backend/utils/misc/guc_tables.c: struct config_string ConfigureNamesString[]
      	{
      		{NULL, 0, 0, NULL, NULL}, NULL, NULL, NULL, NULL, NULL
     
    + ## src/include/libpq/auth.h ##
    +@@
    + 
    + #include "libpq/libpq-be.h"
    + 
    ++/*
    ++ * Maximum accepted size of GSS and SSPI authentication tokens.
    ++ * We also use this as a limit on ordinary password packet lengths.
    ++ *
    ++ * Kerberos tickets are usually quite small, but the TGTs issued by Windows
    ++ * domain controllers include an authorization field known as the Privilege
    ++ * Attribute Certificate (PAC), which contains the user's Windows permissions
    ++ * (group memberships etc.). The PAC is copied into all tickets obtained on
    ++ * the basis of this TGT (even those issued by Unix realms which the Windows
    ++ * realm trusts), and can be several kB in size. The maximum token size
    ++ * accepted by Windows systems is determined by the MaxAuthToken Windows
    ++ * registry setting. Microsoft recommends that it is not set higher than
    ++ * 65535 bytes, so that seems like a reasonable limit for us as well.
    ++ */
    ++#define PG_MAX_AUTH_TOKEN_LENGTH	65535
    ++
    + extern PGDLLIMPORT char *pg_krb_server_keyfile;
    + extern PGDLLIMPORT bool pg_krb_caseins_users;
    + extern PGDLLIMPORT bool pg_gss_accept_deleg;
    +@@ src/include/libpq/auth.h: extern PGDLLIMPORT char *pg_krb_realm;
    + extern void ClientAuthentication(Port *port);
    + extern void sendAuthRequest(Port *port, AuthRequest areq, const char *extradata,
    + 							int extralen);
    ++extern void set_authn_id(Port *port, const char *id);
    + 
    + /* Hook for plugins to get control in ClientAuthentication() */
    + typedef void (*ClientAuthentication_hook_type) (Port *, int);
    +
      ## src/include/libpq/hba.h ##
     @@ src/include/libpq/hba.h: typedef enum UserAuth
    + 	uaLDAP,
      	uaCert,
      	uaRADIUS,
    - 	uaCustom,
     -	uaPeer
     -#define USER_AUTH_LAST uaPeer	/* Must be last value of this enum */
     +	uaPeer,
    @@ src/include/libpq/hba.h: typedef struct HbaLine
     +	char	   *oauth_issuer;
     +	char	   *oauth_scope;
     +	bool		oauth_skip_usermap;
    - 	char	   *custom_provider;
    - 	List	   *custom_auth_options;
      } HbaLine;
    + 
    + typedef struct IdentLine
     
      ## src/include/libpq/oauth.h (new) ##
     @@
 8:  e71df89b8f <  -:  ---------- Add a very simple authn_id extension
 9:  73adeb3645 !  4:  573a2ca3bc Add pytest suite for OAuth
    @@ src/test/python/README (new)
     +A test suite for exercising both the libpq client and the server backend at the
     +protocol level, based on pytest and Construct.
     +
    ++WARNING! This suite takes superuser-level control of the cluster under test,
    ++writing to the server config, creating and destroying databases, etc. It also
    ++spins up various ephemeral TCP services. This is not safe for production servers
    ++and therefore must be explicitly opted into by setting PG_TEST_EXTRA=python in
    ++the environment.
    ++
     +The test suite currently assumes that the standard PG* environment variables
     +point to the database under test and are sufficient to log in a superuser on
     +that system. In other words, a bare `psql` needs to Just Work before the test
    @@ src/test/python/README (new)
     +
     +The first run of
     +
    -+    make installcheck
    ++    make installcheck PG_TEST_EXTRA=python
     +
     +will install a local virtual environment and all needed dependencies. During
     +development, if libpq changes incompatibly, you can issue
    @@ src/test/python/README (new)
     +The Makefile is there for convenience, but you don't have to use it. Activate
     +the virtualenv to be able to use pytest directly:
     +
    ++    $ export PG_TEST_EXTRA=python
     +    $ source venv/bin/activate
     +    $ py.test -k oauth
     +    ...
    @@ src/test/python/client/test_oauth.py (new)
     +    with pytest.raises(psycopg2.OperationalError, match=expected_error):
     +        client.check_completed()
     
    + ## src/test/python/conftest.py (new) ##
    +@@
    ++#
    ++# Copyright 2023 Timescale, Inc.
    ++# SPDX-License-Identifier: PostgreSQL
    ++#
    ++
    ++import os
    ++
    ++import pytest
    ++
    ++
    ++@pytest.fixture(scope="session", autouse=True)
    ++def _check_PG_TEST_EXTRA(request):
    ++    """
    ++    Automatically skips the whole suite if PG_TEST_EXTRA doesn't contain
    ++    'python'. pytestmark doesn't seem to work in a top-level conftest.py, so
    ++    I've made this an autoused fixture instead.
    ++
    ++    TODO: there are tests here that are probably safe, but until I do a full
    ++    analysis on which are and which are not, I've made the entire thing opt-in.
    ++    """
    ++    extra_tests = os.getenv("PG_TEST_EXTRA", "").split()
    ++    if "python" not in extra_tests:
    ++        pytest.skip("Potentially unsafe test 'python' not enabled in PG_TEST_EXTRA")
    +
      ## src/test/python/pq3.py (new) ##
     @@
     +#
    @@ src/test/python/pq3.py (new)
     +
     +# Pq3
     +
    ++
     +# Adapted from construct.core.EnumIntegerString
     +class EnumNamedByte:
     +    def __init__(self, val, name):
    @@ src/test/python/pytest.ini (new)
      ## src/test/python/requirements.txt (new) ##
     @@
     +black
    -+cryptography~=3.4.6
    ++# cryptography 39.x removes a lot of platform support, beware
    ++cryptography~=38.0.4
     +construct~=2.10.61
     +isort~=5.6
    ++# TODO: update to psycopg[c] 3.1
     +psycopg2~=2.8.6
    -+pytest~=6.1
    -+pytest-asyncio~=0.14.0
    ++pytest~=7.3
    ++pytest-asyncio~=0.21.0
     
      ## src/test/python/server/__init__.py (new) ##
     
    @@ src/test/python/server/conftest.py (new)
     +
     +import pq3
     +
    ++BLOCKING_TIMEOUT = 2  # the number of seconds to wait for blocking calls
    ++
     +
     +@pytest.fixture
     +def connect():
    @@ src/test/python/server/conftest.py (new)
     +            addr = (pq3.pghost(), pq3.pgport())
     +
     +            try:
    -+                sock = socket.create_connection(addr, timeout=2)
    ++                sock = socket.create_connection(addr, timeout=BLOCKING_TIMEOUT)
     +            except ConnectionError as e:
     +                pytest.skip(f"unable to connect to {addr}: {e}")
     +
    @@ src/test/python/server/test_oauth.py (new)
     @@
     +#
     +# Copyright 2021 VMware, Inc.
    ++# Portions Copyright 2023 Timescale, Inc.
     +# SPDX-License-Identifier: PostgreSQL
     +#
     +
    @@ src/test/python/server/test_oauth.py (new)
     +
     +import pq3
     +
    ++from .conftest import BLOCKING_TIMEOUT
    ++
     +MAX_SASL_MESSAGE_LENGTH = 65535
     +
     +INVALID_AUTHORIZATION_ERRCODE = b"28000"
    @@ src/test/python/server/test_oauth.py (new)
     +
     +SHARED_MEM_NAME = "oauth-pytest"
     +MAX_TOKEN_SIZE = 4096
    -+MAX_UINT16 = 2 ** 16 - 1
    ++MAX_UINT16 = 2**16 - 1
     +
     +
     +def skip_if_no_postgres():
    @@ src/test/python/server/test_oauth.py (new)
     +    addr = (pq3.pghost(), pq3.pgport())
     +
     +    try:
    -+        with socket.create_connection(addr, timeout=2):
    ++        with socket.create_connection(addr, timeout=BLOCKING_TIMEOUT):
     +            pass
     +    except ConnectionError as e:
     +        pytest.skip(f"unable to connect to {addr}: {e}")
    @@ src/test/python/server/test_oauth.py (new)
     +    return connect()
     +
     +
    -+@pytest.fixture(scope="module", autouse=True)
    -+def authn_id_extension(oauth_ctx):
    -+    """
    -+    Performs a `CREATE EXTENSION authn_id` in the test database. This fixture is
    -+    autoused, so tests don't need to rely on it.
    -+    """
    -+    conn = psycopg2.connect(database=oauth_ctx.dbname)
    -+    conn.autocommit = True
    -+
    -+    with contextlib.closing(conn):
    -+        c = conn.cursor()
    -+        c.execute("CREATE EXTENSION authn_id;")
    -+
    -+
     +@pytest.fixture(scope="session")
     +def shared_mem():
     +    """
    @@ src/test/python/server/test_oauth.py (new)
     +    expect_handshake_success(conn)
     +
     +    # Make sure that the server has not set an authenticated ID.
    -+    pq3.send(conn, pq3.types.Query, query=b"SELECT authn_id();")
    ++    pq3.send(conn, pq3.types.Query, query=b"SELECT system_user;")
     +    resp = receive_until(conn, pq3.types.DataRow)
     +
     +    row = resp.payload
    @@ src/test/python/server/test_oauth.py (new)
     +    expect_handshake_success(conn)
     +
     +    # Check the reported authn_id.
    -+    pq3.send(conn, pq3.types.Query, query=b"SELECT authn_id();")
    ++    pq3.send(conn, pq3.types.Query, query=b"SELECT system_user;")
     +    resp = receive_until(conn, pq3.types.DataRow)
     +
    ++    expected = authn_id
    ++    if expected is not None:
    ++        expected = b"oauth:" + expected
    ++
     +    row = resp.payload
    -+    assert row.columns == [authn_id]
    ++    assert row.columns == [expected]
     +
     +
     +class ExpectedError(object):
    @@ src/test/python/server/test_oauth.py (new)
     +    expect_handshake_success(conn)
     +
     +    # Check the user identity.
    -+    pq3.send(conn, pq3.types.Query, query=b"SELECT authn_id();")
    ++    pq3.send(conn, pq3.types.Query, query=b"SELECT system_user;")
     +    resp = receive_until(conn, pq3.types.DataRow)
     +
     +    row = resp.payload
    -+    expected = oauth_ctx.user.encode("utf-8")
    ++    expected = b"oauth:" + oauth_ctx.user.encode("utf-8")
     +    assert row.columns == [expected]
     +
     +
    @@ src/test/python/server/validate_bearer.py (new)
     +import sys
     +from multiprocessing import shared_memory
     +
    -+MAX_UINT16 = 2 ** 16 - 1
    ++MAX_UINT16 = 2**16 - 1
     +
     +
     +def remove_shm_from_resource_tracker():
10:  ab32128f34 <  -:  ---------- contrib/oauth: switch to pluggable auth API
 -:  ---------- >  5:  4490d029b5 squash! Add pytest suite for OAuth