1: 92cf9bdcb3 = 1: dc523009f2 common/jsonapi: support FRONTEND clients 2: 0a58e64ade = 2: af969e6cea libpq: add OAUTHBEARER SASL mechanism 3: c56bc808b6 = 3: 8906c9d445 backend: add OAUTHBEARER SASL mechanism -: ---------- > 4: e2566ab594 Introduce OAuth validator libraries -: ---------- > 5: 26781a7f15 squash! Introduce OAuth validator libraries 4: 35ca8abdad ! 6: 295de92a5a Add pytest suite for OAuth @@ src/test/python/server/conftest.py (new) + + yield conn_factory + ## src/test/python/server/oauthtest.c (new) ## +@@ ++/*------------------------------------------------------------------------- ++ * ++ * oauthtest.c ++ * Test module for serverside OAuth token validation callbacks ++ * ++ * Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group ++ * Portions Copyright (c) 1994, Regents of the University of California ++ * ++ * src/test/python/server/oauthtest.c ++ * ++ *------------------------------------------------------------------------- ++ */ ++ ++#include "postgres.h" ++ ++#include "fmgr.h" ++#include "libpq/oauth.h" ++#include "utils/guc.h" ++#include "utils/memutils.h" ++ ++PG_MODULE_MAGIC; ++ ++static void test_startup(ValidatorModuleState *state); ++static void test_shutdown(ValidatorModuleState *state); ++static ValidatorModuleResult * test_validate(ValidatorModuleState *state, ++ const char *token, ++ const char *role); ++ ++static const OAuthValidatorCallbacks callbacks = { ++ .startup_cb = test_startup, ++ .shutdown_cb = test_shutdown, ++ .validate_cb = test_validate, ++}; ++ ++static char *expected_bearer = ""; ++static bool set_authn_id = false; ++static char *authn_id = ""; ++static bool reflect_role = false; ++ ++void ++_PG_init(void) ++{ ++ DefineCustomStringVariable("oauthtest.expected_bearer", ++ "Expected Bearer token for future connections", ++ NULL, ++ &expected_bearer, ++ "", ++ PGC_SIGHUP, ++ 0, ++ NULL, NULL, NULL); ++ ++ DefineCustomBoolVariable("oauthtest.set_authn_id", ++ "Whether to set an authenticated identity", ++ NULL, ++ &set_authn_id, ++ false, ++ PGC_SIGHUP, ++ 0, ++ NULL, NULL, NULL); ++ DefineCustomStringVariable("oauthtest.authn_id", ++ "Authenticated identity to use for future connections", ++ NULL, ++ &authn_id, ++ "", ++ PGC_SIGHUP, ++ 0, ++ NULL, NULL, NULL); ++ ++ DefineCustomBoolVariable("oauthtest.reflect_role", ++ "Ignore the bearer token; use the requested role as the authn_id", ++ NULL, ++ &reflect_role, ++ false, ++ PGC_SIGHUP, ++ 0, ++ NULL, NULL, NULL); ++ ++ MarkGUCPrefixReserved("oauthtest"); ++} ++ ++const OAuthValidatorCallbacks * ++_PG_oauth_validator_module_init(void) ++{ ++ return &callbacks; ++} ++ ++static void ++test_startup(ValidatorModuleState *state) ++{ ++} ++ ++static void ++test_shutdown(ValidatorModuleState *state) ++{ ++} ++ ++static ValidatorModuleResult * ++test_validate(ValidatorModuleState *state, const char *token, const char *role) ++{ ++ ValidatorModuleResult *res; ++ ++ res = palloc0(sizeof(ValidatorModuleResult)); /* TODO: palloc context? */ ++ ++ if (reflect_role) ++ { ++ res->authenticated = true; ++ res->authn_id = pstrdup(role); /* TODO: constify? */ ++ } ++ else ++ { ++ if (*expected_bearer && !strcmp(token, expected_bearer)) ++ res->authenticated = true; ++ if (set_authn_id) ++ res->authn_id = authn_id; ++ } ++ ++ return res; ++} + ## src/test/python/server/test_oauth.py (new) ## @@ +# @@ src/test/python/server/test_oauth.py (new) +FEATURE_NOT_SUPPORTED_ERRCODE = b"0A000" + +SHARED_MEM_NAME = "oauth-pytest" -+MAX_TOKEN_SIZE = 4096 +MAX_UINT16 = 2**16 - 1 + + @@ src/test/python/server/test_oauth.py (new) + dbname = "oauth_test_" + id + + user = "oauth_user_" + id ++ punct_user = "oauth_\"'? ;&!_user_" + id # username w/ punctuation + map_user = "oauth_map_user_" + id + authz_user = "oauth_authz_user_" + id + @@ src/test/python/server/test_oauth.py (new) + + # Create our roles and database. + user = sql.Identifier(ctx.user) ++ punct_user = sql.Identifier(ctx.punct_user) + map_user = sql.Identifier(ctx.map_user) + authz_user = sql.Identifier(ctx.authz_user) + dbname = sql.Identifier(ctx.dbname) + + c.execute(sql.SQL("CREATE ROLE {} LOGIN;").format(user)) ++ c.execute(sql.SQL("CREATE ROLE {} LOGIN;").format(punct_user)) + c.execute(sql.SQL("CREATE ROLE {} LOGIN;").format(map_user)) + c.execute(sql.SQL("CREATE ROLE {} LOGIN;").format(authz_user)) + c.execute(sql.SQL("CREATE DATABASE {};").format(dbname)) + -+ # Make this test script the server's oauth_validator. -+ path = pathlib.Path(__file__).parent / "validate_bearer.py" -+ path = str(path.absolute()) -+ -+ cmd = f"{shlex.quote(path)} {SHARED_MEM_NAME} <&%f" -+ c.execute("ALTER SYSTEM SET oauth_validator_command TO %s;", (cmd,)) -+ + # Replace pg_hba and pg_ident. + c.execute("SHOW hba_file;") + hba = c.fetchone()[0] @@ src/test/python/server/test_oauth.py (new) + # Put things back the way they were. + c.execute("SELECT pg_reload_conf();") + -+ c.execute("ALTER SYSTEM RESET oauth_validator_command;") + c.execute(sql.SQL("DROP DATABASE {};").format(dbname)) + c.execute(sql.SQL("DROP ROLE {};").format(authz_user)) + c.execute(sql.SQL("DROP ROLE {};").format(map_user)) ++ c.execute(sql.SQL("DROP ROLE {};").format(punct_user)) + c.execute(sql.SQL("DROP ROLE {};").format(user)) + + @@ src/test/python/server/test_oauth.py (new) + return connect() + + -+@pytest.fixture(scope="session") -+def shared_mem(): -+ """ -+ Yields a shared memory segment that can be used for communication between -+ the bearer_token fixture and ./validate_bearer.py. -+ """ -+ size = MAX_TOKEN_SIZE + 2 # two byte length prefix -+ mem = shared_memory.SharedMemory(SHARED_MEM_NAME, create=True, size=size) -+ -+ try: -+ with contextlib.closing(mem): -+ yield mem -+ finally: -+ mem.unlink() -+ -+ -+@pytest.fixture() -+def bearer_token(shared_mem): ++def bearer_token(*, size=16): + """ -+ Returns a factory function that, when called, will store a Bearer token in -+ shared_mem. If token is None (the default), a new token will be generated -+ using secrets.token_urlsafe() and returned; otherwise the passed token will -+ be used as-is. -+ -+ When token is None, the generated token size in bytes may be specified as an -+ argument; if unset, a small 16-byte token will be generated. The token size -+ may not exceed MAX_TOKEN_SIZE in any case. -+ -+ The return value is the token, converted to a bytes object. -+ -+ As a special case for testing failure modes, accept_any may be set to True. -+ This signals to the validator command that any bearer token should be -+ accepted. The returned token in this case may be used or discarded as needed -+ by the test. ++ Generates a Bearer token using secrets.token_urlsafe(). The generated token ++ size in bytes may be specified; if unset, a small 16-byte token will be ++ generated. + """ + -+ def set_token(token=None, *, size=16, accept_any=False): -+ if token is not None: -+ size = len(token) ++ if size % 4: ++ raise ValueError(f"requested token size {size} is not a multiple of 4") + -+ if size > MAX_TOKEN_SIZE: -+ raise ValueError(f"token size {size} exceeds maximum size {MAX_TOKEN_SIZE}") ++ token = secrets.token_urlsafe(size // 4 * 3) ++ assert len(token) == size + -+ if token is None: -+ if size % 4: -+ raise ValueError(f"requested token size {size} is not a multiple of 4") -+ -+ token = secrets.token_urlsafe(size // 4 * 3) -+ assert len(token) == size -+ -+ try: -+ token = token.encode("ascii") -+ except AttributeError: -+ pass # already encoded -+ -+ if accept_any: -+ # Two-byte magic value. -+ shared_mem.buf[:2] = struct.pack("H", MAX_UINT16) -+ else: -+ # Two-byte length prefix, then the token data. -+ shared_mem.buf[:2] = struct.pack("H", len(token)) -+ shared_mem.buf[2 : size + 2] = token -+ -+ return token -+ -+ return set_token ++ return token + + +def begin_oauth_handshake(conn, oauth_ctx, *, user=None): @@ src/test/python/server/test_oauth.py (new) + ) + + ++@pytest.fixture() ++def setup_validator(): ++ """ ++ A per-test fixture that sets up the test validator with expected behavior. ++ The setting will be reverted during teardown. ++ """ ++ conn = psycopg2.connect("") ++ conn.autocommit = True ++ ++ with contextlib.closing(conn): ++ c = conn.cursor() ++ prev = dict() ++ ++ def setter(**gucs): ++ for guc, val in gucs.items(): ++ # Save the previous value. ++ c.execute(sql.SQL("SHOW oauthtest.{};").format(sql.Identifier(guc))) ++ prev[guc] = c.fetchone()[0] ++ ++ c.execute( ++ sql.SQL("ALTER SYSTEM SET oauthtest.{} TO %s;").format( ++ sql.Identifier(guc) ++ ), ++ (val,), ++ ) ++ c.execute("SELECT pg_reload_conf();") ++ ++ yield setter ++ ++ # Restore the previous values. ++ for guc, val in prev.items(): ++ c.execute( ++ sql.SQL("ALTER SYSTEM SET oauthtest.{} TO %s;").format( ++ sql.Identifier(guc) ++ ), ++ (val,), ++ ) ++ c.execute("SELECT pg_reload_conf();") ++ ++ +@pytest.mark.parametrize("token_len", [16, 1024, 4096]) +@pytest.mark.parametrize( + "auth_prefix", @@ src/test/python/server/test_oauth.py (new) + b"Bearer ", + ], +) -+def test_oauth(conn, oauth_ctx, bearer_token, auth_prefix, token_len): -+ begin_oauth_handshake(conn, oauth_ctx) -+ ++def test_oauth(setup_validator, connect, oauth_ctx, auth_prefix, token_len): + # Generate our bearer token with the desired length. + token = bearer_token(size=token_len) -+ auth = auth_prefix + token ++ setup_validator(expected_bearer=token) ++ ++ conn = connect() ++ begin_oauth_handshake(conn, oauth_ctx) + ++ auth = auth_prefix + token.encode("ascii") + send_initial_response(conn, auth=auth) + expect_handshake_success(conn) + @@ src/test/python/server/test_oauth.py (new) + "x-._~+/x", + ], +) -+def test_oauth_bearer_corner_cases(conn, oauth_ctx, bearer_token, token_value): ++def test_oauth_bearer_corner_cases(setup_validator, connect, oauth_ctx, token_value): ++ setup_validator(expected_bearer=token_value) ++ ++ conn = connect() + begin_oauth_handshake(conn, oauth_ctx) + -+ send_initial_response(conn, bearer=bearer_token(token_value)) ++ send_initial_response(conn, bearer=token_value.encode("ascii")) + + expect_handshake_success(conn) + @@ src/test/python/server/test_oauth.py (new) + ), + ], +) -+def test_oauth_authn_id(conn, oauth_ctx, bearer_token, user, authn_id, should_succeed): -+ token = None -+ ++def test_oauth_authn_id( ++ setup_validator, connect, oauth_ctx, user, authn_id, should_succeed ++): ++ token = bearer_token() + authn_id = authn_id(oauth_ctx) -+ if authn_id is not None: -+ authn_id = authn_id.encode("ascii") + -+ # As a hack to get the validator to reflect arbitrary output from this -+ # test, encode the desired output as a base64 token. The validator will -+ # key on the leading "output=" to differentiate this from the random -+ # tokens generated by secrets.token_urlsafe(). -+ output = b"output=" + authn_id + b"\n" -+ token = base64.urlsafe_b64encode(output) ++ # Set up the validator appropriately. ++ gucs = dict(expected_bearer=token) ++ if authn_id is not None: ++ gucs["set_authn_id"] = True ++ gucs["authn_id"] = authn_id ++ setup_validator(**gucs) + -+ token = bearer_token(token) ++ conn = connect() + username = user(oauth_ctx) -+ + begin_oauth_handshake(conn, oauth_ctx, user=username) -+ send_initial_response(conn, bearer=token) ++ send_initial_response(conn, bearer=token.encode("ascii")) + + if not should_succeed: + expect_handshake_failure(conn, oauth_ctx) @@ src/test/python/server/test_oauth.py (new) + + expected = authn_id + if expected is not None: -+ expected = b"oauth:" + expected ++ expected = b"oauth:" + expected.encode("ascii") + + row = resp.payload + assert row.columns == [expected] @@ src/test/python/server/test_oauth.py (new) + assert expected in detail + + -+def test_oauth_rejected_bearer(conn, oauth_ctx, bearer_token): -+ # Generate a new bearer token, which we will proceed not to use. -+ _ = bearer_token() -+ ++def test_oauth_rejected_bearer(conn, oauth_ctx): + begin_oauth_handshake(conn, oauth_ctx) + + # Send a bearer token that doesn't match what the validator expects. It @@ src/test/python/server/test_oauth.py (new) + b"Bearer ", + b"Bearer a===b", + b"Bearer hello!", ++ b"Bearer trailingspace ", ++ b"Bearer trailingtab\t", + b"Bearer me@example.com", ++ b"Beare abcd", + b'OAuth realm="Example"', + b"", + ], +) -+def test_oauth_invalid_bearer(conn, oauth_ctx, bearer_token, bad_bearer): ++def test_oauth_invalid_bearer(setup_validator, connect, oauth_ctx, bad_bearer): + # Tell the validator to accept any token. This ensures that the invalid + # bearer tokens are rejected before the validation step. -+ _ = bearer_token(accept_any=True) ++ setup_validator(reflect_role=True) + ++ conn = connect() + begin_oauth_handshake(conn, oauth_ctx) + send_initial_response(conn, auth=bad_bearer) + @@ src/test/python/server/test_oauth.py (new) + err.match(resp) + + -+def test_oauth_empty_initial_response(conn, oauth_ctx, bearer_token): ++def test_oauth_empty_initial_response(setup_validator, connect, oauth_ctx): ++ token = bearer_token() ++ setup_validator(expected_bearer=token) ++ ++ conn = connect() + begin_oauth_handshake(conn, oauth_ctx) + + # Send an initial response without data. @@ src/test/python/server/test_oauth.py (new) + assert not pkt.payload.body + + # Now send the initial data. -+ data = b"n,,\x01auth=Bearer " + bearer_token() + b"\x01\x01" ++ data = b"n,,\x01auth=Bearer " + token.encode("ascii") + b"\x01\x01" + pq3.send(conn, pq3.types.PasswordMessage, data) + + # Server should now complete the handshake. + expect_handshake_success(conn) + + -+@pytest.fixture() -+def set_validator(): -+ """ -+ A per-test fixture that allows a test to override the setting of -+ oauth_validator_command for the cluster. The setting will be reverted during -+ teardown. -+ -+ Passing None will perform an ALTER SYSTEM RESET. -+ """ -+ conn = psycopg2.connect("") -+ conn.autocommit = True -+ -+ with contextlib.closing(conn): -+ c = conn.cursor() -+ -+ # Save the previous value. -+ c.execute("SHOW oauth_validator_command;") -+ prev_cmd = c.fetchone()[0] -+ -+ def setter(cmd): -+ c.execute("ALTER SYSTEM SET oauth_validator_command TO %s;", (cmd,)) -+ c.execute("SELECT pg_reload_conf();") -+ -+ yield setter -+ -+ # Restore the previous value. -+ c.execute("ALTER SYSTEM SET oauth_validator_command TO %s;", (prev_cmd,)) -+ c.execute("SELECT pg_reload_conf();") -+ -+ -+def test_oauth_no_validator(oauth_ctx, set_validator, connect, bearer_token): ++# TODO: see if there's a way to test this easily after the API switch ++def xtest_oauth_no_validator(setup_validator, oauth_ctx, connect): + # Clear out our validator command, then establish a new connection. + set_validator("") + conn = connect() @@ src/test/python/server/test_oauth.py (new) + expect_handshake_failure(conn, oauth_ctx) + + -+def test_oauth_validator_role(oauth_ctx, set_validator, connect): -+ # Switch the validator implementation. This validator will reflect the -+ # PGUSER as the authenticated identity. -+ path = pathlib.Path(__file__).parent / "validate_reflect.py" -+ path = str(path.absolute()) ++@pytest.mark.parametrize( ++ "user", ++ [ ++ pytest.param( ++ lambda ctx: ctx.user, ++ id="basic username", ++ ), ++ pytest.param( ++ lambda ctx: ctx.punct_user, ++ id="'unsafe' characters are passed through correctly", ++ ), ++ ], ++) ++def test_oauth_validator_role(setup_validator, oauth_ctx, connect, user): ++ username = user(oauth_ctx) + -+ set_validator(f"{shlex.quote(path)} '%r' <&%f") ++ # Tell the validator to reflect the PGUSER as the authenticated identity. ++ setup_validator(reflect_role=True) + conn = connect() + -+ # Log in. Note that the reflection validator ignores the bearer token. -+ begin_oauth_handshake(conn, oauth_ctx, user=oauth_ctx.user) ++ # Log in. Note that reflection ignores the bearer token. ++ begin_oauth_handshake(conn, oauth_ctx, user=username) + send_initial_response(conn, bearer=b"dontcare") + expect_handshake_success(conn) + @@ src/test/python/server/test_oauth.py (new) + resp = receive_until(conn, pq3.types.DataRow) + + row = resp.payload -+ expected = b"oauth:" + oauth_ctx.user.encode("utf-8") ++ expected = b"oauth:" + username.encode("utf-8") + assert row.columns == [expected] -+ -+ -+def test_oauth_role_with_shell_unsafe_characters(oauth_ctx, set_validator, connect): -+ """ -+ XXX This test pins undesirable behavior. We should be able to handle any -+ valid Postgres role name. -+ """ -+ # Switch the validator implementation. This validator will reflect the -+ # PGUSER as the authenticated identity. -+ path = pathlib.Path(__file__).parent / "validate_reflect.py" -+ path = str(path.absolute()) -+ -+ set_validator(f"{shlex.quote(path)} '%r' <&%f") -+ conn = connect() -+ -+ unsafe_username = "hello'there" -+ begin_oauth_handshake(conn, oauth_ctx, user=unsafe_username) -+ -+ # The server should reject the handshake. -+ send_initial_response(conn, bearer=b"dontcare") -+ expect_handshake_failure(conn, oauth_ctx) ## src/test/python/server/test_server.py (new) ## @@ @@ src/test/python/server/test_server.py (new) + resp = pq3.recv1(conn) + assert resp.type == pq3.types.ReadyForQuery - ## src/test/python/server/validate_bearer.py (new) ## -@@ -+#! /usr/bin/env python3 -+# -+# Copyright 2021 VMware, Inc. -+# SPDX-License-Identifier: PostgreSQL -+# -+# DO NOT USE THIS OAUTH VALIDATOR IN PRODUCTION. It doesn't actually validate -+# anything, and it logs the bearer token data, which is sensitive. -+# -+# This executable is used as an oauth_validator_command in concert with -+# test_oauth.py. Memory is shared and communicated from that test module's -+# bearer_token() fixture. -+# -+# This script must run under the Postgres server environment; keep the -+# dependency list fairly standard. -+ -+import base64 -+import binascii -+import contextlib -+import struct -+import sys -+from multiprocessing import shared_memory -+ -+MAX_UINT16 = 2**16 - 1 -+ -+ -+def remove_shm_from_resource_tracker(): -+ """ -+ Monkey-patch multiprocessing.resource_tracker so SharedMemory won't be -+ tracked. Pulled from this thread, where there are more details: -+ -+ https://bugs.python.org/issue38119 -+ -+ TL;DR: all clients of shared memory segments automatically destroy them on -+ process exit, which makes shared memory segments much less useful. This -+ monkeypatch removes that behavior so that we can defer to the test to manage -+ the segment lifetime. -+ -+ Ideally a future Python patch will pull in this fix and then the entire -+ function can go away. -+ """ -+ from multiprocessing import resource_tracker -+ -+ def fix_register(name, rtype): -+ if rtype == "shared_memory": -+ return -+ return resource_tracker._resource_tracker.register(self, name, rtype) -+ -+ resource_tracker.register = fix_register -+ -+ def fix_unregister(name, rtype): -+ if rtype == "shared_memory": -+ return -+ return resource_tracker._resource_tracker.unregister(self, name, rtype) -+ -+ resource_tracker.unregister = fix_unregister -+ -+ if "shared_memory" in resource_tracker._CLEANUP_FUNCS: -+ del resource_tracker._CLEANUP_FUNCS["shared_memory"] -+ -+ -+def main(args): -+ remove_shm_from_resource_tracker() # XXX remove some day -+ -+ # Get the expected token from the currently running test. -+ shared_mem_name = args[0] -+ -+ mem = shared_memory.SharedMemory(shared_mem_name) -+ with contextlib.closing(mem): -+ # First two bytes are the token length. -+ size = struct.unpack("H", mem.buf[:2])[0] -+ -+ if size == MAX_UINT16: -+ # Special case: the test wants us to accept any token. -+ sys.stderr.write("accepting token without validation\n") -+ return -+ -+ # The remainder of the buffer contains the expected token. -+ assert size <= (mem.size - 2) -+ expected_token = mem.buf[2 : size + 2].tobytes() -+ -+ mem.buf[:] = b"\0" * mem.size # scribble over the token -+ -+ token = sys.stdin.buffer.read() -+ if token != expected_token: -+ sys.exit(f"failed to match Bearer token ({token!r} != {expected_token!r})") -+ -+ # See if the test wants us to print anything. If so, it will have encoded -+ # the desired output in the token with an "output=" prefix. -+ try: -+ # altchars="-_" corresponds to the urlsafe alphabet. -+ data = base64.b64decode(token, altchars="-_", validate=True) -+ -+ if data.startswith(b"output="): -+ sys.stdout.buffer.write(data[7:]) -+ -+ except binascii.Error: -+ pass -+ -+ -+if __name__ == "__main__": -+ main(sys.argv[1:]) - - ## src/test/python/server/validate_reflect.py (new) ## -@@ -+#! /usr/bin/env python3 -+# -+# Copyright 2021 VMware, Inc. -+# SPDX-License-Identifier: PostgreSQL -+# -+# DO NOT USE THIS OAUTH VALIDATOR IN PRODUCTION. It ignores the bearer token -+# entirely and automatically logs the user in. -+# -+# This executable is used as an oauth_validator_command in concert with -+# test_oauth.py. It expects the user's desired role name as an argument; the -+# actual token will be discarded and the user will be logged in with the role -+# name as the authenticated identity. -+# -+# This script must run under the Postgres server environment; keep the -+# dependency list fairly standard. -+ -+import sys -+ -+ -+def main(args): -+ # We have to read the entire token as our first action to unblock the -+ # server, but we won't actually use it. -+ _ = sys.stdin.buffer.read() -+ -+ if len(args) != 1: -+ sys.exit("usage: ./validate_reflect.py ROLE") -+ -+ # Log the user in as the provided role. -+ role = args[0] -+ print(role) -+ -+ -+if __name__ == "__main__": -+ main(sys.argv[1:]) - ## src/test/python/test_internals.py (new) ## @@ +# 5: fb4cac4e99 ! 7: 7d21be13c0 squash! Add pytest suite for OAuth @@ src/test/python/meson.build (new) @@ +# Copyright (c) 2023, PostgreSQL Global Development Group + ++subdir('server') ++ +pytest_env = { + 'with_oauth': oauth_library, + @@ src/test/python/server/conftest.py: import pq3 + f"-c port={port}", + "-c listen_addresses=localhost", + "-c log_connections=on", ++ "-c shared_preload_libraries=oauthtest", ++ "-c oauth_validator_library=oauthtest", + ] + ), + "start", @@ src/test/python/server/conftest.py: import pq3 # Have ExitStack close our socket. stack.enter_context(sock) + ## src/test/python/server/meson.build (new) ## +@@ ++# Copyright (c) 2024, PostgreSQL Global Development Group ++ ++if not oauth.found() ++ subdir_done() ++endif ++ ++oauthtest_sources = files( ++ 'oauthtest.c', ++) ++ ++if host_system == 'windows' ++ oauthtest_sources += rc_lib_gen.process(win32ver_rc, extra_args: [ ++ '--NAME', 'oauthtest', ++ '--FILEDESC', 'passthrough module to validate OAuth tests', ++ ]) ++endif ++ ++oauthtest = shared_module('oauthtest', ++ oauthtest_sources, ++ kwargs: pg_test_mod_args, ++) ++test_install_libs += oauthtest + ## src/test/python/server/test_oauth.py ## -@@ src/test/python/server/test_oauth.py: MAX_TOKEN_SIZE = 4096 +@@ src/test/python/server/test_oauth.py: SHARED_MEM_NAME = "oauth-pytest" MAX_UINT16 = 2**16 - 1 @@ src/test/python/server/test_oauth.py: def oauth_ctx(): conn.autocommit = True with contextlib.closing(conn): -@@ src/test/python/server/test_oauth.py: def test_oauth_empty_initial_response(conn, oauth_ctx, bearer_token): +@@ src/test/python/server/test_oauth.py: def receive_until(conn, type): @pytest.fixture() --def set_validator(): -+def set_validator(postgres_instance): +-def setup_validator(): ++def setup_validator(postgres_instance): """ - A per-test fixture that allows a test to override the setting of - oauth_validator_command for the cluster. The setting will be reverted during -@@ src/test/python/server/test_oauth.py: def set_validator(): - - Passing None will perform an ALTER SYSTEM RESET. + A per-test fixture that sets up the test validator with expected behavior. + The setting will be reverted during teardown. """ - conn = psycopg2.connect("") + host, port = postgres_instance 6: 2008e60b3c ! 8: 26dcd5f828 XXX temporary patches to build and test @@ Commit message 0001; has something changed? - construct 2.10.70 has some incompatibilities with the current tests + ## src/bin/pg_combinebackup/Makefile ## +@@ src/bin/pg_combinebackup/Makefile: include $(top_builddir)/src/Makefile.global + + override CPPFLAGS := -I$(libpq_srcdir) $(CPPFLAGS) + LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils ++# TODO: fix this properly ++LDFLAGS_INTERNAL += -lpgcommon $(libpq_pgport) + + OBJS = \ + $(WIN32RES) \ +@@ src/bin/pg_combinebackup/Makefile: OBJS = \ + + all: pg_combinebackup + +-pg_combinebackup: $(OBJS) | submake-libpgport submake-libpgfeutils +- $(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(libpq_pgport) $(LIBS) -o $@$(X) ++pg_combinebackup: $(OBJS) | submake-libpq submake-libpgport submake-libpgfeutils ++ $(CC) $(CFLAGS) $^ $(LDFLAGS) $(LDFLAGS_EX) $(LIBS) -o $@$(X) + + install: all installdirs + $(INSTALL_PROGRAM) pg_combinebackup$(X) '$(DESTDIR)$(bindir)/pg_combinebackup$(X)' + ## src/bin/pg_combinebackup/meson.build ## @@ src/bin/pg_combinebackup/meson.build: endif @@ src/bin/pg_combinebackup/meson.build: endif ) bin_targets += pg_combinebackup + ## src/bin/pg_verifybackup/Makefile ## +@@ src/bin/pg_verifybackup/Makefile: top_builddir = ../../.. + include $(top_builddir)/src/Makefile.global + + # We need libpq only because fe_utils does. +-LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils $(libpq_pgport) ++LDFLAGS_INTERNAL += -L$(top_builddir)/src/fe_utils -lpgfeutils -lpgcommon $(libpq_pgport) + + OBJS = \ + $(WIN32RES) \ + ## src/test/python/requirements.txt ## @@ black 7: 64611d33ef = 9: 0ff8e3786a REVERT: temporarily skip the exit check