On Fri, Sep 23, 2022 at 3:39 PM Jacob Champion <[email protected]> wrote: > Here's a newly rebased v5. (They're all zipped now, which I probably > should have done a while back, sorry.)
To keep this current, v7 is rebased over latest, without the pluggable authentication patches. This doesn't yet address the architectural feedback that was discussed previously, so if you're primarily interested in that, you can safely ignore this version of the patchset. The key changes here include - Meson support, for both the build and the pytest suite - Cirrus support (and unsurprisingly, Mac and Windows builds fail due to the Linux-oriented draft code) - A small tweak to support iddawc down to 0.9.8 (shipped with e.g. Debian Bullseye) - Removal of the authn_id test extension in favor of SYSTEM_USER The meson+pytest support was big enough that I split it into its own patch. It's not very polished yet, but it mostly works, and when running tests via Meson it'll now spin up a test server for you. My virtualenv approach apparently interacts poorly with the multiarch Cirrus setup (64-bit tests pass, 32-bit tests fail). Moving forward, the first thing I plan to tackle is asynchronous operation, so that polling clients can still operate sanely. If I can find a good solution there, the conversations about possible extension points should get a lot easier. Thanks, --Jacob
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
++
++
[email protected](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
++
+
[email protected]
+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()
+
+
[email protected](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;")
-+
-+
[email protected](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
v7-0001-common-jsonapi-support-FRONTEND-clients.patch.gz
Description: application/gzip
v7-0003-backend-add-OAUTHBEARER-SASL-mechanism.patch.gz
Description: application/gzip
v7-0004-Add-pytest-suite-for-OAuth.patch.gz
Description: application/gzip
v7-0002-libpq-add-OAUTHBEARER-SASL-mechanism.patch.gz
Description: application/gzip
v7-0005-squash-Add-pytest-suite-for-OAuth.patch.gz
Description: application/gzip
