since-v15.diff.txt

text/plain

Filename: since-v15.diff.txt
Type: text/plain
Part: 0
Message: Re: [PoC] Federated Authn/z with OAUTHBEARER
 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