since-v15.diff.txt
text/plain
Filename: since-v15.diff.txt
Type: text/plain
Part: 0
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