pgjdbc/pgjdbc GitHub issues and pull requests (mirror)  
help / color / mirror / Atom feed
From: vlsi (@vlsi) <[email protected]>
To: pgjdbc/pgjdbc <[email protected]>
Subject: [pgjdbc/pgjdbc] PR #4166: refactor(test-gss): convert to Java/JUnit 5 submodule of the main build
Date: Sun, 14 Jun 2026 14:22:36 +0000
Message-ID: <[email protected]> (raw)

## Why

The `test-gss` module was a standalone Groovy application with its own
Gradle wrapper.  That design required a separate CI step, a prior
`publishToMavenLocal` to make the driver reachable, and a `groovy-all`
dependency that generated constant renovate noise.  Fixes #4130.

## What

- Replaced the five Groovy scripts with Java/JUnit 5 classes under
  `test-gss/src/test/java` (`GssEncryptionTest`, `Kerberos`, `Postgres`,
  `PgGssConnection`, `GssTestUtil`).
- Removed the embedded Gradle wrapper, `settings.gradle`, and
  `build.gradle` from `test-gss/`.
- Added `test-gss/build.gradle.kts` using the `build-logic.java-library`
  and `build-logic.test-junit5` conventions.  The `test` task sets the
  required GSSAPI system properties and environment variables, and uses a
  working directory under `build/gss-test/` to keep the source tree
  clean.
- `settings.gradle.kts` now unconditionally includes `:test-gss`, so
  `compileTestJava` always runs (catching Java errors on every CI job).
  The `test` task itself is guarded by `onlyIf` and only executes when
  `-PgssTests` is passed.
- CI (`main.yml`): the Kerberos and host-file setup steps now run before
  the main `Test` step.  The separate "Test GSS" Gradle action is
  removed; the shared step appends `-PgssTests` inline when
  `matrix.gss == 'yes'`.
- `matrix.mjs`: removed the GSS condition from `deploy_to_maven_local`
  since `:test-gss` now depends directly on `:postgresql` without a
  local Maven publish.

## How to verify

On a Linux host with `krb5-kdc` and PostgreSQL 16 installed:

```
./gradlew :test-gss:test -PgssTests
```

Without `-PgssTests`, `:test-gss:compileTestJava` compiles normally and
`:test-gss:test` is reported as `SKIPPED`.

diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 6dd805f655..936d5ff10c 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -295,6 +295,21 @@ jobs:
         EOF
         fi
 
+    - name: 'Install krb5 for GSS tests'
+      if: ${{ matrix.gss == 'yes' }}
+      # language=bash
+      run: |
+        sudo apt -y update
+        sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
+        sudo apt -y install postgresql-16
+        sudo apt -y install krb5-kdc krb5-admin-server libkrb5-dev postgresql
+    - name: 'Update hosts for GSS tests'
+      if: ${{ matrix.gss == 'yes' }}
+      # language=bash
+      run: |
+        sudo sed -i '/^127\.0\.0\.1/c\127.0.0.1 auth-test-localhost.postgresql.example.com localhost' /etc/hosts
+        cat /etc/hosts
+
     - uses: burrunan/gradle-cache-action@4b67497abd37a511d6c1dc6299bdd84ff39f7bf5 # v3.0.2
       name: Test
       env:
@@ -304,7 +319,7 @@ jobs:
       with:
         read-only: false
         job-id: jdk${{ matrix.java_version }}
-        arguments: --scan --no-parallel --no-daemon jandex test ${{ matrix.extraGradleArgs }}
+        arguments: --scan --no-parallel --no-daemon jandex test ${{ matrix.gss == 'yes' && '-PgssTests' || '' }} ${{ matrix.extraGradleArgs }}
         properties: |
           includeTestTags=${{ matrix.includeTestTags }}
           testExtraJvmArgs=${{ matrix.testExtraJvmArgs }}
@@ -314,20 +329,6 @@ jobs:
           # We provision JDKs with GitHub Actions for caching purposes, so Gradle should rather fail in case JDK is not found
           org.gradle.java.installations.auto-download=false
 
-    - name: 'Install krb5 for GSS tests'
-      if: ${{ matrix.gss == 'yes' }}
-      # language=bash
-      run: |
-        sudo apt -y update
-        sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y
-        sudo apt -y install postgresql-16
-        sudo apt -y install krb5-kdc krb5-admin-server libkrb5-dev postgresql
-    - name: 'Update hosts for GSS tests'
-      if: ${{ matrix.gss == 'yes' }}
-      # language=bash
-      run: |
-        sudo -- sh -c "echo 127.0.0.1 auth-test-localhost.postgresql.example.com localhost > /etc/hosts"
-        cat /etc/hosts
     - uses: burrunan/gradle-cache-action@4b67497abd37a511d6c1dc6299bdd84ff39f7bf5 # v3.0.2
       if: ${{ matrix.deploy_to_maven_local }}
       name: Deploy pgjdbc to mavenLocal
@@ -394,17 +395,6 @@ jobs:
       with:
         name: ${{ steps.artifact_name.outputs.name }}
         path: ${{ runner.temp }}/pg-server-logs.txt
-    - name: Test GSS
-      if: ${{ matrix.gss == 'yes' }}
-      # language=bash
-      run: |
-        cd test-gss
-        ./gradlew assemble
-        ./gradlew run
-      env:
-        KRB5CCNAME: /home/runner/work/pgjdbc/pgjdbc/test-gss/tmp_check/krb5cc
-        KRB5_CONFIG: /home/runner/work/pgjdbc/pgjdbc/test-gss/tmp_check/krb5.conf
-        KRB5_KDC_PROFILE: /home/runner/work/pgjdbc/pgjdbc/test-gss/tmp_check/kdc.conf
     - name: Test anorm-sbt
       if: ${{ matrix.check_anorm_sbt == 'yes' }}
       # language=bash
diff --git a/.github/workflows/matrix.mjs b/.github/workflows/matrix.mjs
index c62824f748..5c5f16de8b 100644
--- a/.github/workflows/matrix.mjs
+++ b/.github/workflows/matrix.mjs
@@ -384,7 +384,7 @@ include.forEach(v => {
 
   v.includeTestTags = includeTestTags.join(' | ');
 
-  if (v.gss === 'yes' || v.check_anorm_sbt === 'yes') {
+  if (v.check_anorm_sbt === 'yes') {
       v.deploy_to_maven_local = true
   }
   if (v.hash.value === 'same') {
diff --git a/pgjdbc-gss-test/build.gradle.kts b/pgjdbc-gss-test/build.gradle.kts
new file mode 100644
index 0000000000..759d60abc0
--- /dev/null
+++ b/pgjdbc-gss-test/build.gradle.kts
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+plugins {
+    id("build-logic.java-library")
+    id("build-logic.test-junit5")
+}
+
+dependencies {
+    testImplementation(projects.postgresql)
+}
+
+// The GSS test spawns a local Kerberos KDC and PostgreSQL server, then connects over GSSAPI.
+// The module is always compiled, but the test only runs when -PgssTests is passed, since it needs a
+// Kerberos toolchain and a local PostgreSQL that are only available in the CI "gss" matrix job.
+val gssWorkDir = layout.buildDirectory.dir("gss-test")
+val gssTestsEnabled = providers.gradleProperty("gssTests").isPresent
+
+tasks.test {
+    onlyIf("GSS test runs only when -PgssTests is passed") { gssTestsEnabled }
+    // The Kerberos config files and credential cache are created under this directory at runtime,
+    // so it lives under build/ to keep the source tree clean and to be ignored by git.
+    workingDir = gssWorkDir.get().asFile
+    doFirst {
+        workingDir.mkdirs()
+    }
+    // Use the OS-native GSSAPI so the server-side Kerberos setup is honoured.
+    systemProperty("sun.security.jgss.native", "true")
+    systemProperty("javax.security.auth.useSubjectCredsOnly", "false")
+    systemProperty(
+        "java.security.auth.login.config",
+        layout.projectDirectory.file("jaas.conf").asFile.absolutePath
+    )
+    // The native GSSAPI implementation reads the Kerberos configuration and credential cache from
+    // the environment, so the paths must match the ones created by the test under tmp_check.
+    environment("KRB5CCNAME", gssWorkDir.get().file("tmp_check/krb5cc").asFile.absolutePath)
+    environment("KRB5_CONFIG", gssWorkDir.get().file("tmp_check/krb5.conf").asFile.absolutePath)
+    environment("KRB5_KDC_PROFILE", gssWorkDir.get().file("tmp_check/kdc.conf").asFile.absolutePath)
+}
diff --git a/test-gss/jaas.conf b/pgjdbc-gss-test/jaas.conf
similarity index 100%
rename from test-gss/jaas.conf
rename to pgjdbc-gss-test/jaas.conf
diff --git a/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/GssEncryptionTest.java b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/GssEncryptionTest.java
new file mode 100644
index 0000000000..ff3ae98e94
--- /dev/null
+++ b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/GssEncryptionTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.gss;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.postgresql.PGProperty;
+import org.postgresql.jdbc.GSSEncMode;
+
+import org.junit.jupiter.api.Test;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Locale;
+
+/**
+ * End-to-end GSSAPI test: it boots a local Kerberos KDC and PostgreSQL server, then verifies that
+ * pgjdbc can authenticate over GSSAPI both with and without connection encryption, including the
+ * case where the Kerberos principal is mapped to a different database user.
+ *
+ * <p>The module is only built when {@code -PgssTests} is passed, and the test relies on a Kerberos
+ * toolchain (krb5-kdc, kadmin, kinit) and a local PostgreSQL installation, so in practice it runs
+ * in the dedicated CI "gss" job on Linux.
+ */
+class GssEncryptionTest {
+  private static final String KERBEROS_HOST = "auth-test-localhost.postgresql.example.com";
+
+  @Test
+  void gssAuthenticationAndEncryption() throws Exception {
+    String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT);
+    boolean isMac = osName.contains("mac");
+
+    Kerberos kerberos = new Kerberos();
+    kerberos.startKerberos();
+
+    Process pg = null;
+    try {
+      Postgres postgres = isMac
+          ? new Postgres()
+          : new Postgres("/usr/lib/postgresql/16/bin/", "/tmp/pggss");
+
+      // Make sure we can connect with a trusted superuser before tightening the rules
+      postgres.writePgHba("host    all             all             127.0.0.1/32            trust");
+      assertTrue(postgres.waitForHba(5000), "pg_hba.conf was not created");
+
+      pg = postgres.startPostgres(kerberos.getEnvironment());
+      Thread.sleep(2000);
+
+      String superUser = System.getProperty("user.name");
+      String superPass = "test";
+
+      PgGssConnection client = new PgGssConnection("127.0.0.1", postgres.getPort());
+      client.addProperty(PGProperty.GSS_ENC_MODE, GSSEncMode.DISABLE.value);
+      client.createUser(superUser, superPass, "test1", "secret1");
+      client.createUser(superUser, superPass, "test2", "secret2");
+      client.createDatabase(superUser, superPass, "test1", "test");
+      client.createDatabase(superUser, superPass, "test2", "test2");
+
+      postgres.enableGss("127.0.0.1", "hostgssenc");
+      postgres.enableMyMap("EXAMPLE.COM");
+      postgres.setKeyTabLocation(kerberos.getKeytab());
+      postgres.reload();
+
+      client.addProperty(PGProperty.GSS_ENC_MODE, GSSEncMode.REQUIRE.value);
+      client.addProperty(PGProperty.JAAS_LOGIN, true);
+      client.addProperty(PGProperty.JAAS_APPLICATION_NAME, "pgjdbc");
+
+      // The Kerberos principal test1 maps to the database user test1
+      assertGssConnection(client, postgres, "test", "test1", "secret1",
+          "SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid()",
+          "GSS authenticated and encrypted");
+
+      // The Kerberos principal test1 now maps to a different database user, test2
+      postgres.enableOwnerMap("test1", "EXAMPLE.COM", "test2");
+      postgres.reload();
+      assertGssConnection(client, postgres, "test2", "test2", "secret2",
+          "SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid()",
+          "GSS authenticated and encrypted");
+
+      // GSS authentication without connection encryption
+      postgres.enableMyMap("EXAMPLE.COM");
+      postgres.enableGss("127.0.0.1", "hostnogssenc");
+      postgres.reload();
+      client.addProperty(PGProperty.GSS_ENC_MODE, GSSEncMode.DISABLE.value);
+      assertGssConnection(client, postgres, "test", "test1", "secret1",
+          "SELECT gss_authenticated AND not encrypted from pg_stat_gssapi where pid = pg_backend_pid()",
+          "GSS authenticated and not encrypted");
+    } finally {
+      if (pg != null) {
+        pg.destroy();
+      }
+      kerberos.destroy();
+    }
+  }
+
+  private static void assertGssConnection(PgGssConnection client, Postgres postgres, String database,
+      String user, String password, String query, String description) throws Exception {
+    try (Connection connection =
+             client.tryConnect(database, KERBEROS_HOST, postgres.getPort(), user, password)) {
+      assertTrue(client.select(connection, query),
+          description + " connection failed; pg_hba.conf:\n" + postgres.readPgHba());
+      System.err.println(description + " connection succeeded");
+    } catch (SQLException ex) {
+      System.err.println("pg_hba.conf:\n" + postgres.readPgHba());
+      throw ex;
+    }
+  }
+}
diff --git a/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/GssTestUtil.java b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/GssTestUtil.java
new file mode 100644
index 0000000000..90aa371532
--- /dev/null
+++ b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/GssTestUtil.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.gss;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.ServerSocket;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Small helpers shared by the GSS test: free-port lookup, file read/write and process spawning.
+ */
+final class GssTestUtil {
+  private GssTestUtil() {
+  }
+
+  /**
+   * Returns a free TCP port. There is an inherent race between closing the socket and the caller
+   * binding the port, however it matches the behaviour the test relied on before.
+   */
+  static int findFreePort() throws IOException {
+    try (ServerSocket socket = new ServerSocket(0)) {
+      return socket.getLocalPort();
+    }
+  }
+
+  /**
+   * Appends {@code text} followed by a newline to {@code fileName}. When {@code truncate} is set the
+   * file is overwritten instead, so a single call fully replaces the previous content (this is how
+   * pg_hba.conf and pg_ident.conf are rewritten between scenarios).
+   */
+  static void writeText(String fileName, String text, boolean truncate) throws IOException {
+    Path path = Paths.get(fileName);
+    byte[] bytes = (text + "\n ").getBytes(StandardCharsets.UTF_8);
+    if (truncate) {
+      Files.write(path, bytes);
+    } else {
+      Files.write(path, bytes, StandardOpenOption.CREATE, StandardOpenOption.APPEND);
+    }
+  }
+
+  static String readText(String fileName) throws IOException {
+    return new String(Files.readAllBytes(Paths.get(fileName)), StandardCharsets.UTF_8);
+  }
+
+  /**
+   * Runs a command, forwarding its output to this JVM, and waits for it to finish.
+   */
+  static int runAndWait(List<String> command, Map<String, String> env)
+      throws IOException, InterruptedException {
+    Process process = start(command, env);
+    process.getOutputStream().close();
+    return process.waitFor();
+  }
+
+  /**
+   * Starts a (typically long-running) command, forwarding its output to this JVM, without waiting.
+   */
+  static Process start(List<String> command, Map<String, String> env) throws IOException {
+    ProcessBuilder builder = new ProcessBuilder(command);
+    if (env != null) {
+      builder.environment().putAll(env);
+    }
+    builder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+    builder.redirectError(ProcessBuilder.Redirect.INHERIT);
+    return builder.start();
+  }
+
+  static void deleteRecursively(File file) {
+    File[] children = file.listFiles();
+    if (children != null) {
+      for (File child : children) {
+        deleteRecursively(child);
+      }
+    }
+    // Best effort: leftover files in build/ are not fatal for the test
+    file.delete();
+  }
+
+  /**
+   * Writes {@code text} to the process standard input and closes it.
+   */
+  static void writeStdin(Process process, String text) throws IOException {
+    try (OutputStream stdin = process.getOutputStream()) {
+      stdin.write(text.getBytes(StandardCharsets.UTF_8));
+    }
+  }
+}
diff --git a/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/Kerberos.java b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/Kerberos.java
new file mode 100644
index 0000000000..e27ab2a4f6
--- /dev/null
+++ b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/Kerberos.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.gss;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Sets up a throwaway Kerberos KDC for the GSS test: it writes krb5.conf / kdc.conf, creates the
+ * realm database, the {@code test1} principal and the {@code postgres/<host>} service principal,
+ * starts the KDC and obtains an initial ticket via {@code kinit}.
+ */
+class Kerberos {
+  private static final String HOST = "auth-test-localhost.postgresql.example.com";
+  private static final String HOST_ADDR = "127.0.0.1";
+  private static final String REALM = "EXAMPLE.COM";
+
+  private String krb5BinDir;
+  private String krb5SbinDir;
+  private String kinit = "kinit";
+  private String kdb5Util = "kdb5_util";
+  private String kadminLocal = "kadmin.local";
+  private String krb5kdc = "krb5kdc";
+
+  private String krb5Conf;
+  private String kdcConf;
+  private String kdcCache;
+  private String krb5Log;
+  private String kdcLog;
+  private int kdcPort;
+  private String kdcDataDir;
+  private String kdcPidfile;
+  private String keytab;
+
+  private final Map<String, String> env = new LinkedHashMap<>();
+  private Process krb5Process;
+
+  String getKeytab() {
+    return keytab;
+  }
+
+  Map<String, String> getEnvironment() {
+    return env;
+  }
+
+  private void getBinDir() {
+    String osName = System.getProperty("os.name").toLowerCase(Locale.ROOT);
+    if (osName.contains("mac")) {
+      krb5BinDir = "/usr/local/opt/krb5/bin";
+      krb5SbinDir = "/usr/local/opt/krb5/sbin";
+    } else if (osName.contains("freebsd")) {
+      krb5BinDir = "/usr/local/bin";
+      krb5SbinDir = "/usr/local/sbin";
+    } else if (osName.contains("linux")) {
+      krb5BinDir = "/usr/bin";
+      krb5SbinDir = "/usr/sbin";
+    }
+  }
+
+  private void setupKerberos(String testLib) throws IOException {
+    if (krb5BinDir != null && new File(krb5BinDir).exists()) {
+      kinit = krb5BinDir + "/" + kinit;
+    }
+    if (krb5SbinDir != null && new File(krb5SbinDir).exists()) {
+      kdb5Util = krb5SbinDir + "/" + kdb5Util;
+      kadminLocal = krb5SbinDir + "/" + kadminLocal;
+      krb5kdc = krb5SbinDir + "/" + krb5kdc;
+    }
+
+    String tmpCheck = testLib + "/tmp_check";
+    krb5Conf = tmpCheck + "/krb5.conf";
+    kdcConf = tmpCheck + "/kdc.conf";
+    kdcCache = tmpCheck + "/krb5cc";
+    krb5Log = tmpCheck + "/log/krb5libs.log";
+    kdcLog = tmpCheck + "/log/krb5kdc.log";
+    kdcPort = GssTestUtil.findFreePort();
+    kdcDataDir = tmpCheck + "/krb5kdc";
+    kdcPidfile = tmpCheck + "/krb5kdc.pid";
+    keytab = tmpCheck + "/krb5.keytab";
+
+    System.err.println("setting up Kerberos");
+
+    new File(tmpCheck).mkdirs();
+    new File(tmpCheck, "log").mkdirs();
+
+    Files.deleteIfExists(Paths.get(krb5Conf));
+    Files.deleteIfExists(Paths.get(kdcConf));
+
+    GssTestUtil.writeText(krb5Conf,
+        "[logging]\n"
+            + "default = FILE:" + krb5Log + "\n"
+            + "kdc = FILE:" + kdcLog + "\n"
+            + "\n"
+            + "[libdefaults]\n"
+            + "default_realm = " + REALM + "\n"
+            + "canonicalize = true\n"
+            + "\n"
+            + "[realms]\n"
+            + REALM + " = {\n"
+            + "    kdc = " + HOST_ADDR + ":" + kdcPort + "\n"
+            + "}",
+        true);
+
+    // For new-enough versions of krb5 (1.15+) the *_listen settings let us bind to localhost only.
+    GssTestUtil.writeText(kdcConf,
+        "[kdcdefaults]\n"
+            + "kdc_listen = " + HOST_ADDR + ":" + kdcPort + "\n"
+            + "kdc_tcp_listen = " + HOST_ADDR + ":" + kdcPort + "\n"
+            + "\n"
+            + "[realms]\n"
+            + REALM + " = {\n"
+            + "    database_name = " + kdcDataDir + "/principal\n"
+            + "    admin_keytab = FILE:" + kdcDataDir + "/kadm5.keytab\n"
+            + "    acl_file = " + kdcDataDir + "/kadm5.acl\n"
+            + "    key_stash_file = " + kdcDataDir + "/_k5." + REALM + "\n"
+            + "}",
+        true);
+  }
+
+  private void runKerberos() throws IOException, InterruptedException {
+    mkdir(kdcDataDir);
+
+    env.put("KRB5_CONFIG", krb5Conf);
+    env.put("KRB5_KDC_PROFILE", kdcConf);
+    env.put("KRB5CCNAME", kdcCache);
+
+    String servicePrincipal = "postgres/" + HOST;
+    String test1Password = "secret1";
+
+    GssTestUtil.runAndWait(
+        Arrays.asList(kdb5Util, "create", "-s", "-P", "secret0"), env);
+    GssTestUtil.runAndWait(
+        Arrays.asList(kadminLocal, "-q", "addprinc -pw " + test1Password + " test1"), env);
+    GssTestUtil.runAndWait(
+        Arrays.asList(kadminLocal, "-q", "addprinc -randkey " + servicePrincipal), env);
+    GssTestUtil.runAndWait(
+        Arrays.asList(kadminLocal, "-q", "ktadd -k " + keytab + " " + servicePrincipal), env);
+
+    krb5Process = GssTestUtil.start(Arrays.asList(krb5kdc, "-P", kdcPidfile), env);
+    // Give the KDC a moment to bind before requesting a ticket
+    Thread.sleep(1000);
+
+    Process kinitProcess = GssTestUtil.start(Arrays.asList(kinit, "test1"), env);
+    GssTestUtil.writeStdin(kinitProcess, test1Password + "\n");
+    kinitProcess.waitFor();
+  }
+
+  private void mkdir(String newDir) {
+    File dir = new File(newDir);
+    if (dir.exists()) {
+      GssTestUtil.deleteRecursively(dir);
+    }
+    dir.mkdirs();
+    dir.deleteOnExit();
+  }
+
+  void destroy() {
+    try {
+      String pid = GssTestUtil.readText(kdcPidfile).trim();
+      GssTestUtil.runAndWait(Arrays.asList("kill", "-TERM", pid), null);
+    } catch (IOException | InterruptedException ex) {
+      System.err.println("Unable to stop the KDC: " + ex);
+    }
+    if (krb5Process != null) {
+      krb5Process.destroy();
+    }
+    new File(keytab).delete();
+  }
+
+  void startKerberos() throws IOException, InterruptedException {
+    getBinDir();
+    String curDir = System.getProperty("user.dir");
+    setupKerberos(curDir);
+    // Tell the pure-Java Kerberos implementation where to look (native GSSAPI uses KRB5_CONFIG)
+    System.setProperty("java.security.krb5.conf", krb5Conf);
+    runKerberos();
+  }
+}
diff --git a/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/PgGssConnection.java b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/PgGssConnection.java
new file mode 100644
index 0000000000..a579648fac
--- /dev/null
+++ b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/PgGssConnection.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.gss;
+
+import org.postgresql.PGProperty;
+
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.Properties;
+
+/**
+ * Thin JDBC helper used by the GSS test to provision users/databases and to open the connections
+ * whose GSS authentication and encryption status is then asserted.
+ */
+class PgGssConnection {
+  private final String host;
+  private final int port;
+  private final Properties properties = new Properties();
+
+  PgGssConnection(String host, int port) {
+    this.host = host;
+    this.port = port;
+  }
+
+  void addProperty(PGProperty property, String value) {
+    property.set(properties, value);
+  }
+
+  void addProperty(PGProperty property, boolean value) {
+    property.set(properties, value);
+  }
+
+  Connection tryConnect(String database, String host, int port, String user, String password)
+      throws SQLException {
+    String url = "jdbc:postgresql://" + host + ":" + port + "/" + database;
+    PGProperty.USER.set(properties, user);
+    PGProperty.PASSWORD.set(properties, password);
+    return DriverManager.getConnection(url, properties);
+  }
+
+  /**
+   * Runs {@code query} (which is expected to return a single boolean column) and returns its value.
+   */
+  boolean select(Connection connection, String query) throws SQLException {
+    try (Statement statement = connection.createStatement();
+         ResultSet resultSet = statement.executeQuery(query)) {
+      return resultSet.next() && resultSet.getBoolean(1);
+    }
+  }
+
+  void createUser(String superuser, String superPassword, String user, String password)
+      throws SQLException {
+    String url = "jdbc:postgresql://" + host + ":" + port + "/postgres";
+    PGProperty.USER.set(properties, superuser);
+    PGProperty.PASSWORD.set(properties, superPassword);
+    try (Connection connection = DriverManager.getConnection(url, properties);
+         Statement statement = connection.createStatement()) {
+      try (ResultSet resultSet =
+               statement.executeQuery("select * from pg_user where usename = '" + user + "'")) {
+        if (resultSet.next()) {
+          return;
+        }
+      }
+      statement.execute("create user " + user + " with password '" + password + "'");
+    }
+  }
+
+  void createDatabase(String superuser, String superPassword, String owner, String database)
+      throws SQLException {
+    String url = "jdbc:postgresql://" + host + ":" + port + "/postgres";
+    PGProperty.USER.set(properties, superuser);
+    PGProperty.PASSWORD.set(properties, superPassword);
+    try (Connection connection = DriverManager.getConnection(url, properties);
+         Statement statement = connection.createStatement()) {
+      try (ResultSet resultSet =
+               statement.executeQuery("select * from pg_database where datname = '" + database + "'")) {
+        if (resultSet.next()) {
+          return;
+        }
+      }
+      statement.execute("create database " + database + " owner '" + owner + "'");
+    }
+  }
+}
diff --git a/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/Postgres.java b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/Postgres.java
new file mode 100644
index 0000000000..9bf635acf5
--- /dev/null
+++ b/pgjdbc-gss-test/src/test/java/org/postgresql/test/gss/Postgres.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (c) 2026, PostgreSQL Global Development Group
+ * See the LICENSE file in the project root for more information.
+ */
+
+package org.postgresql.test.gss;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * Drives a throwaway PostgreSQL server for the GSS test: initialises a data directory, starts the
+ * server and rewrites pg_hba.conf / pg_ident.conf / postgresql.conf to toggle GSS behaviour.
+ */
+class Postgres {
+  private final String binPath;
+  private final String dataPath;
+  private final String hostName = "127.0.0.1";
+  private int port;
+
+  Postgres() throws IOException, InterruptedException {
+    this("/usr/local/pgsql/16/bin/", "/tmp/pgdata16");
+  }
+
+  Postgres(String binDir, String dataDir) throws IOException, InterruptedException {
+    this.binPath = binDir;
+    this.dataPath = dataDir;
+    initDb();
+  }
+
+  private void initDb() throws IOException, InterruptedException {
+    if (!new File(dataPath).exists()) {
+      System.err.println("Initializing db at " + dataPath);
+      GssTestUtil.runAndWait(
+          Arrays.asList(binPath + "/initdb", "--auth=trust", "-D", dataPath), null);
+    }
+  }
+
+  int getPort() {
+    return port;
+  }
+
+  /**
+   * Picks a free port and starts the server, passing the Kerberos environment so the backend can
+   * locate its configuration and keytab.
+   */
+  Process startPostgres(Map<String, String> krb5Env) throws IOException {
+    port = GssTestUtil.findFreePort();
+    // -i enables TCP connections (JDBC needs them); -k /tmp keeps the unix socket out of the data dir
+    System.err.println(
+        "executing postgres datapath: " + dataPath + ", host: " + hostName + ", port: " + port);
+    return GssTestUtil.start(
+        Arrays.asList(binPath + "/postgres", "-h", hostName, "-k", "/tmp",
+            "-p", Integer.toString(port), "-i", "-D", dataPath),
+        krb5Env);
+  }
+
+  void reload() throws IOException, InterruptedException {
+    GssTestUtil.runAndWait(
+        Arrays.asList(binPath + "/pg_ctl", "-D", dataPath, "reload"), null);
+  }
+
+  boolean waitForHba(int milliseconds) {
+    long deadline = System.nanoTime() + (long) (milliseconds * 1E6);
+    while (System.nanoTime() < deadline) {
+      if (new File(dataPath, "pg_hba.conf").exists()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  void writePgHba(String text) throws IOException {
+    GssTestUtil.writeText(dataPath + "/pg_hba.conf", text, true);
+  }
+
+  String readPgHba() throws IOException {
+    return GssTestUtil.readText(dataPath + "/pg_hba.conf");
+  }
+
+  private void writePgIdent(String text) throws IOException {
+    GssTestUtil.writeText(dataPath + "/pg_ident.conf", text, true);
+  }
+
+  private void writePgConf(String text) throws IOException {
+    GssTestUtil.writeText(dataPath + "/postgresql.conf", text, false);
+  }
+
+  void setKeyTabLocation(String location) throws IOException {
+    writePgConf("krb_server_keyfile = '" + location + "'");
+  }
+
+  void enableGss(String hostAddress, String mode) throws IOException {
+    writePgHba(mode + " all all " + hostAddress + "/32 gss map=mymap");
+  }
+
+  void enableMyMap(String realm) throws IOException {
+    writePgIdent("mymap  /^(.*)@" + realm + "$  \\1");
+  }
+
+  /** Maps the Kerberos principal to a database user that differs from the Kerberos login user. */
+  void enableOwnerMap(String principal, String realm, String user) throws IOException {
+    writePgIdent("mymap " + principal + "@" + realm + "  " + user);
+  }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 8da6e3abed..91c547e66c 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -47,6 +47,7 @@ if (providers.gradleProperty("jdkTestVersion").orNull?.toInt() != 8) {
 }
 include("postgresql")
 include("testkit")
+include("pgjdbc-gss-test")
 
 project(":postgresql").projectDir = file("pgjdbc")
 
diff --git a/test-gss/build.gradle b/test-gss/build.gradle
deleted file mode 100644
index 9ab8eff705..0000000000
--- a/test-gss/build.gradle
+++ /dev/null
@@ -1,24 +0,0 @@
-plugins {
-    id 'groovy'
-    id 'java'
-    id 'application'
-}
-
-group 'org.example'
-version '1.0-SNAPSHOT'
-
-repositories {
-    mavenCentral()
-    mavenLocal()
-}
-
-dependencies {
-    implementation('org.codehaus.groovy:groovy-all:3.0.25')
-    implementation(group: 'org.postgresql', name: 'postgresql', version: '1.0.0-dev-master-SNAPSHOT')
-    testImplementation(group: 'junit', name: 'junit', version: '4.13.2')
-}
-application {
-    mainClass = 'TestPostgres'
-    applicationDefaultJvmArgs = ['-Djava.security.auth.login.config=jaas.conf']
-}
-
diff --git a/test-gss/gradle/wrapper/gradle-wrapper.jar b/test-gss/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index b1b8ef56b4..0000000000
Binary files a/test-gss/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/test-gss/gradle/wrapper/gradle-wrapper.properties b/test-gss/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index bd82f36bab..0000000000
--- a/test-gss/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,10 +0,0 @@
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-distributionSha256Sum=bafc141b619ad6350fd975fc903156dd5c151998cc8b058e8c1044ab5f7b031f
-distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
-networkTimeout=10000
-retries=0
-retryBackOffMs=500
-validateDistributionUrl=true
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
diff --git a/test-gss/gradlew b/test-gss/gradlew
deleted file mode 100755
index b9bb139f79..0000000000
--- a/test-gss/gradlew
+++ /dev/null
@@ -1,248 +0,0 @@
-#!/bin/sh
-
-#
-# Copyright © 2015 the original authors.
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#      https://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-# SPDX-License-Identifier: Apache-2.0
-#
-
-##############################################################################
-#
-#   Gradle start up script for POSIX generated by Gradle.
-#
-#   Important for running:
-#
-#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
-#       noncompliant, but you have some other compliant shell such as ksh or
-#       bash, then to run this script, type that shell name before the whole
-#       command line, like:
-#
-#           ksh Gradle
-#
-#       Busybox and similar reduced shells will NOT work, because this script
-#       requires all of these POSIX shell features:
-#         * functions;
-#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
-#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
-#         * compound commands having a testable exit status, especially «case»;
-#         * various built-in commands including «command», «set», and «ulimit».
-#
-#   Important for patching:
-#
-#   (2) This script targets any POSIX shell, so it avoids extensions provided
-#       by Bash, Ksh, etc; in particular arrays are avoided.
-#
-#       The "traditional" practice of packing multiple parameters into a
-#       space-separated string is a well documented source of bugs and security
-#       problems, so this is (mostly) avoided, by progressively accumulating
-#       options in "$@", and eventually passing that to Java.
-#
-#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
-#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
-#       see the in-line comments for details.
-#
-#       There are tweaks for specific operating systems such as AIX, CygWin,
-#       Darwin, MinGW, and NonStop.
-#
-#   (3) This script is generated from the Groovy template
-#       https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins...
-#       within the Gradle project.
-#
-#       You can find Gradle at https://github.com/gradle/gradle/.
-#
-##############################################################################
-
-# Attempt to set APP_HOME
-
-# Resolve links: $0 may be a link
-app_path=$0
-
-# Need this for daisy-chained symlinks.
-while
-    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
-    [ -h "$app_path" ]
-do
-    ls=$( ls -ld "$app_path" )
-    link=${ls#*' -> '}
-    case $link in             #(
-      /*)   app_path=$link ;; #(
-      *)    app_path=$APP_HOME$link ;;
-    esac
-done
-
-# This is normally unused
-# shellcheck disable=SC2034
-APP_BASE_NAME=${0##*/}
-# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD=maximum
-
-warn () {
-    echo "$*"
-} >&2
-
-die () {
-    echo
-    echo "$*"
-    echo
-    exit 1
-} >&2
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-nonstop=false
-case "$( uname )" in                #(
-  CYGWIN* )         cygwin=true  ;; #(
-  Darwin* )         darwin=true  ;; #(
-  MSYS* | MINGW* )  msys=true    ;; #(
-  NONSTOP* )        nonstop=true ;;
-esac
-
-
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
-    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
-        # IBM's JDK on AIX uses strange locations for the executables
-        JAVACMD=$JAVA_HOME/jre/sh/java
-    else
-        JAVACMD=$JAVA_HOME/bin/java
-    fi
-    if [ ! -x "$JAVACMD" ] ; then
-        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-else
-    JAVACMD=java
-    if ! command -v java >/dev/null 2>&1
-    then
-        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-    fi
-fi
-
-# Increase the maximum file descriptors if we can.
-if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
-    case $MAX_FD in #(
-      max*)
-        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC2039,SC3045
-        MAX_FD=$( ulimit -H -n ) ||
-            warn "Could not query maximum file descriptor limit"
-    esac
-    case $MAX_FD in  #(
-      '' | soft) :;; #(
-      *)
-        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
-        # shellcheck disable=SC2039,SC3045
-        ulimit -n "$MAX_FD" ||
-            warn "Could not set maximum file descriptor limit to $MAX_FD"
-    esac
-fi
-
-# Collect all arguments for the java command, stacking in reverse order:
-#   * args from the command line
-#   * the main class name
-#   * -classpath
-#   * -D...appname settings
-#   * --module-path (only if needed)
-#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
-
-# For Cygwin or MSYS, switch paths to Windows format before running java
-if "$cygwin" || "$msys" ; then
-    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
-
-    JAVACMD=$( cygpath --unix "$JAVACMD" )
-
-    # Now convert the arguments - kludge to limit ourselves to /bin/sh
-    for arg do
-        if
-            case $arg in                                #(
-              -*)   false ;;                            # don't mess with options #(
-              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
-                    [ -e "$t" ] ;;                      #(
-              *)    false ;;
-            esac
-        then
-            arg=$( cygpath --path --ignore --mixed "$arg" )
-        fi
-        # Roll the args list around exactly as many times as the number of
-        # args, so each arg winds up back in the position where it started, but
-        # possibly modified.
-        #
-        # NB: a `for` loop captures its iteration list before it begins, so
-        # changing the positional parameters here affects neither the number of
-        # iterations, nor the values presented in `arg`.
-        shift                   # remove old arg
-        set -- "$@" "$arg"      # push replacement arg
-    done
-fi
-
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
-
-# Collect all arguments for the java command:
-#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
-#     and any embedded shellness will be escaped.
-#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
-#     treated as '${Hostname}' itself on the command line.
-
-set -- \
-        "-Dorg.gradle.appname=$APP_BASE_NAME" \
-        -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
-        "$@"
-
-# Stop when "xargs" is not available.
-if ! command -v xargs >/dev/null 2>&1
-then
-    die "xargs is not available"
-fi
-
-# Use "xargs" to parse quoted args.
-#
-# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
-#
-# In Bash we could simply go:
-#
-#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
-#   set -- "${ARGS[@]}" "$@"
-#
-# but POSIX shell has neither arrays nor command substitution, so instead we
-# post-process each arg (as a line of input to sed) to backslash-escape any
-# character that might be a shell metacharacter, then use eval to reverse
-# that process (while maintaining the separation between arguments), and wrap
-# the whole thing up as a single "set" statement.
-#
-# This will of course break if any of these variables contains a newline or
-# an unmatched quote.
-#
-
-eval "set -- $(
-        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
-        xargs -n1 |
-        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
-        tr '\n' ' '
-    )" '"$@"'
-
-exec "$JAVACMD" "$@"
diff --git a/test-gss/gradlew.bat b/test-gss/gradlew.bat
deleted file mode 100644
index 24c62d56f2..0000000000
--- a/test-gss/gradlew.bat
+++ /dev/null
@@ -1,82 +0,0 @@
-@rem
-@rem Copyright 2015 the original author or authors.
-@rem
-@rem Licensed under the Apache License, Version 2.0 (the "License");
-@rem you may not use this file except in compliance with the License.
-@rem You may obtain a copy of the License at
-@rem
-@rem      https://www.apache.org/licenses/LICENSE-2.0
-@rem
-@rem Unless required by applicable law or agreed to in writing, software
-@rem distributed under the License is distributed on an "AS IS" BASIS,
-@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-@rem See the License for the specific language governing permissions and
-@rem limitations under the License.
-@rem
-@rem SPDX-License-Identifier: Apache-2.0
-@rem
-
-@if "%DEBUG%"=="" @echo off
-@rem ##########################################################################
-@rem
-@rem  Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables, and ensure extensions are enabled
-setlocal EnableExtensions
-
-set DIRNAME=%~dp0
-if "%DIRNAME%"=="" set DIRNAME=.
-@rem This is normally unused
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Resolve any "." and ".." in APP_HOME to make it shorter.
-for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if %ERRORLEVEL% equ 0 goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-"%COMSPEC%" /c exit 1
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto execute
-
-echo. 1>&2
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
-echo. 1>&2
-echo Please set the JAVA_HOME variable in your environment to match the 1>&2
-echo location of your Java installation. 1>&2
-
-"%COMSPEC%" /c exit 1
-
-:execute
-@rem Setup the command line
-
-
-
-@rem Execute Gradle
-@rem endlocal doesn't take effect until after the line is parsed and variables are expanded
-@rem which allows us to clear the local environment before executing the java command
-endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
-
-:exitWithErrorLevel
-@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
-"%COMSPEC%" /c exit %ERRORLEVEL%
diff --git a/test-gss/settings.gradle b/test-gss/settings.gradle
deleted file mode 100644
index 61f18d563f..0000000000
--- a/test-gss/settings.gradle
+++ /dev/null
@@ -1,2 +0,0 @@
-rootProject.name = 'test-gss'
-
diff --git a/test-gss/src/main/groovy/Kerberos.groovy b/test-gss/src/main/groovy/Kerberos.groovy
deleted file mode 100644
index f24c5cdee3..0000000000
--- a/test-gss/src/main/groovy/Kerberos.groovy
+++ /dev/null
@@ -1,251 +0,0 @@
[email protected]
-class Kerberos {
-    String krb5BinDir
-    String krb5SbinDir
-    String host     = 'auth-test-localhost.postgresql.example.com'
-    String hostaddr = '127.0.0.1'
-    String realm    = 'EXAMPLE.COM'
-    String krb5Conf
-    String kdcConf
-    String kdcCache
-    String krb5Log
-    String kdcLog
-    String krb5Config  = 'krb5-config'
-    String kinit       = 'kinit'
-    String kdb5Util    = 'kdb5_util'
-    String kadminLocal = 'kadmin.local'
-    String krb5kdc     = 'krb5kdc'
-    String[] env
-    // find a free port
-    int kdcPort
-    String kdcDataDir
-    String kdcPidfile
-    String keytab
-    String krb5Version = '1.16'
-    Process krb5Process
-
-
-    public String[] getEnvironment() {
-        return env
-    }
-    public void getBinDir() {
-        String osName = "uname -s".execute().text
-
-        if (osName.toLowerCase() =~ 'darwin')
-        {
-            krb5BinDir  = '/usr/local/opt/krb5/bin'
-            krb5SbinDir = '/usr/local/opt/krb5/sbin'
-        }
-        else if (osName.toLowerCase() =~ 'freebsd')
-        {
-            krb5BinDir  = '/usr/local/bin'
-            krb5SbinDir = '/usr/local/sbin'
-        }
-        else if (osName.toLowerCase() =~ 'linux')
-        {
-            krb5BinDir  = '/usr/bin'
-            krb5SbinDir = '/usr/sbin'
-        }
-    }
-
-    public void setupKerberos(String testLib) {
-
-        if ( krb5BinDir && new File(krb5BinDir).exists() )
-        {
-            krb5Config = "$krb5BinDir/$krb5Config"
-            kinit      = "$krb5BinDir/$kinit"
-        }
-        if ( krb5SbinDir && new File(krb5SbinDir).exists() )
-        {
-            kdb5Util    = "$krb5SbinDir/$kdb5Util"
-            kadminLocal = "$krb5SbinDir/$kadminLocal"
-            krb5kdc     = "$krb5SbinDir/$krb5kdc"
-        }
-
-
-
-        krb5Conf   = "$testLib/tmp_check/krb5.conf"
-        kdcConf    = "$testLib/tmp_check/kdc.conf"
-        kdcCache   = "$testLib/tmp_check/krb5cc"
-        krb5Log    = "$testLib/tmp_check/log/krb5libs.log"
-        kdcLog     = "$testLib/tmp_check/log/krb5kdc.log"
-        // find a free port
-        kdcPort    = Util.findPort()
-        kdcDataDir = "$testLib/tmp_check/krb5kdc"
-        kdcPidfile = "$testLib/tmp_check/krb5kdc.pid"
-        keytab     = "$testLib/tmp_check/krb5.keytab"
-        krb5Version = '1.16'
-
-        println "setting up Kerberos"
-
-        String tmp = "$krb5Config --version".execute().text
-        if (tmp.startsWith('heimdal')) {
-            println "Heimdal not supported"
-        }
-        tmp =~ 'Kerberos 5 release ([0-9]+\\.[0-9]+)'
-        new File("$testLib/tmp_check").mkdir()
-        new File("$testLib/tmp_check/log").mkdir()
-
-        new File(krb5Conf).with { f->
-            if (f.exists()) {
-                f.delete()
-            }
-            f.createNewFile()
-
-        }
-        new File(kdcConf).with { f->
-            if (f.exists()) {
-                f.delete()
-            }
-            f.createNewFile()
-
-        }
-        Util.appendToFile( krb5Conf,
-"""[logging]
-default = FILE:$krb5Log
-kdc = FILE:$kdcLog
-
-[libdefaults]
-default_realm = $realm
-canonicalize = true
-
-
-[realms]
-    $realm = {
-    kdc = $hostaddr:$kdcPort
- }
-"""
-        )
-
-        Util.appendToFile( kdcConf,"[kdcdefaults]")
-
-
-        // For new-enough versions of krb5, use the _listen settings rather
-        // than the _ports settings so that we can bind to localhost only.
-
-        if (krb5Version >= '1.15')
-        {
-            Util.appendToFile(kdcConf,
-"""
-kdc_listen = $hostaddr:$kdcPort
-kdc_tcp_listen = $hostaddr:$kdcPort
-"""                     )
-        }
-        else
-        {
-            Util.appendToFile( kdcConf,
-"""
-kdc_ports = $kdcPort
-kdc_tcp_ports = $kdcPort
-"""                     )
-        }
-        Util.appendToFile(
-                kdcConf,
-"""
-[realms]
-$realm = {
-    database_name = $kdcDataDir/principal
-    admin_keytab = FILE:$kdcDataDir/kadm5.keytab
-    acl_file = $kdcDataDir/kadm5.acl
-    key_stash_file = $kdcDataDir/_k5.$realm
-}"""                )
-
-    }
-    public Process runKerberos() {
-
-        mkdir(kdcDataDir)
-
-        env = ["KRB5_CONFIG=$krb5Conf", "KRB5_KDC_PROFILE=$kdcConf", "KRB5CCNAME=$kdcCache"]
-
-        String service_principal = "postgres/$host" // should really get postgres from configuration file
-
-        Process p = "$kdb5Util create -s -P secret0".execute(env,null)
-        p.waitForProcessOutput(System.out, System.err)
-
-        String test1Password = 'secret1'
-
-        p = ["$kadminLocal", "-q", "addprinc -pw $test1Password test1" ].execute(env, null)
-        p.waitForProcessOutput(System.out, System.err)
-
-        p = ["$kadminLocal", "-q", "addprinc -randkey $service_principal"].execute(env,null)
-        p.waitForProcessOutput(System.out, System.err)
-
-        p = ["$kadminLocal", "-q", "ktadd -k $keytab $service_principal"].execute(env,null)
-        p.waitForProcessOutput(System.out, System.err)
-
-        new Thread() {
-            @Override
-            void run() {
-                krb5Process = "$krb5kdc -P $kdcPidfile".execute(env,null)
-                krb5Process.waitForProcessOutput(System.out, System.err)
-            }
-        }.start()
-        while (krb5Process == null){
-            Thread.sleep(100)
-        }
-        p = "$kinit test1".execute(env, null)
-        p.withWriter { w ->
-            w.println(test1Password)
-        }
-        p.waitForProcessOutput(System.out, System.err)
-
-        return krb5Process
-
-    }
-
-
-    public void mkdir(String newDir){
-
-        new File(newDir).with {dir->
-            if ( dir.exists() ){
-                dir.deleteDir()
-            }
-            dir.mkdir()
-            dir.deleteOnExit()
-        }
-    }
-    public void destroy() {
-        new File(kdcPidfile).with {f->
-            Process p = "kill -TERM ${f.text}".execute()
-            p.waitForProcessOutput(System.out, System.err)
-        }
-        new File(keytab).with {k->
-            if (k.exists()) {
-                k.delete()
-            }
-        }
-    }
-
-    public Process startKerberos() {
-        getBinDir()
-        String curDir = new File('.').getAbsolutePath()
-        setupKerberos(curDir)
-        // tell java where we want it to look
-        System.setProperty("java.security.krb5.conf", krb5Conf)
-        runKerberos()
-    }
-
-    public void showConfig() {
-        println "krb config file: "
-        new File(krb5Conf).readLines().each {l->
-            println l
-        }
-    }
-
-    public void showKdc() {
-        println "kdc config file: "
-        new File(kdcConf).readLines().each {l->
-            println l
-        }
-    }
-    public static void main(String []args) {
-        Kerberos kerberos = new Kerberos()
-        kerberos.getBinDir()
-        String curDir = new File('.').getAbsolutePath()
-        kerberos.setupKerberos(curDir)
-        kerberos.runKerberos()
-        kerberos.destroy();
-        println(kerberos.krb5BinDir)
-        println(kerberos.krb5SbinDir)
-    }
-}
diff --git a/test-gss/src/main/groovy/PgJDBC.groovy b/test-gss/src/main/groovy/PgJDBC.groovy
deleted file mode 100644
index d69241ff85..0000000000
--- a/test-gss/src/main/groovy/PgJDBC.groovy
+++ /dev/null
@@ -1,83 +0,0 @@
-import org.postgresql.PGProperty
-
-import javax.xml.transform.stream.StreamResult
-import java.sql.Connection
-import java.sql.DriverManager
-import java.sql.ResultSet
-import java.sql.Statement
-
[email protected]
-public class PgJDBC {
-
-    String host
-    int port
-
-    Properties properties = new Properties()
-
-    public PgJDBC() {
-
-    }
-    public PgJDBC(String host, int port) {
-        this.host = host
-        this.port = port
-    }
-
-    public void addProperty(PGProperty pgProperty, Object value) {
-        if (value instanceof String) {
-            pgProperty.set(properties, (String)value)
-        }else if (value instanceof Boolean ) {
-            pgProperty.set(properties, (Boolean)value)
-        }
-
-    }
-
-    public Connection tryConnect(String dataBase, String host, int port, String user, String password) throws Exception {
-        String url = "jdbc:postgresql://$host:$port/$dataBase"
-        PGProperty.USER.set(properties,user)
-        PGProperty.PASSWORD.set(properties,password)
-        DriverManager.getConnection(url,properties)
-
-    }
-
-    public boolean select(Connection connection, String query ) throws Exception {
-        Statement statement
-        try {
-            statement = connection.createStatement()
-            ResultSet resultSet = statement.executeQuery(query)
-            return resultSet.next()
-        } finally {
-            statement.close()
-        }
-        return false;
-    }
-
-
-    public void createUser(String superuser, String superPass, String user, String password) {
-        String url = "jdbc:postgresql://$host:$port/postgres"
-        PGProperty.USER.set(properties, superuser)
-        PGProperty.PASSWORD.set(properties,superPass)
-        Connection conn = DriverManager.getConnection(url,properties)
-        ResultSet rs = conn.createStatement().executeQuery("select * from pg_user where usename = '$user'")
-        if (!rs.next()) {
-            conn.createStatement().execute("create user $user with password '$password'")
-        }
-        conn.close()
-    }
-
-
-    public void createDatabase(String superuser, String superPass, String owner, String database) {
-        String url = "jdbc:postgresql://$host:$port/postgres"
-        PGProperty.USER.set(properties, superuser)
-        PGProperty.PASSWORD.set(properties,superPass)
-        Connection conn = DriverManager.getConnection(url,properties)
-        ResultSet rs = conn.createStatement().executeQuery("select * from pg_database where datname = '$database'")
-        if (!rs.next()) {
-            conn.createStatement().execute("create database $database owner '$owner'")
-        }
-        conn.close()
-    }
-    public static void main(String[] args ){
-        PgJDBC pgJDBC = new PgJDBC()
-        pgJDBC.tryConnect("test", "localhost", 5432, "test", "test")
-    }
-}
diff --git a/test-gss/src/main/groovy/Postgres.groovy b/test-gss/src/main/groovy/Postgres.groovy
deleted file mode 100644
index 3d81fe034d..0000000000
--- a/test-gss/src/main/groovy/Postgres.groovy
+++ /dev/null
@@ -1,110 +0,0 @@
[email protected]
-class Postgres {
-    String binPath
-    String dataPath
-    String hostName = '127.0.0.1'
-    int port
-
-    public Postgres() {
-        setupPaths('/usr/local/pgsql/16/bin/','/tmp/pgdata16')
-        initDB()
-    }
-    public Postgres(String binDir, String dataDir) {
-        setupPaths(binDir, dataDir)
-        initDB()
-    }
-
-    public void setupPaths(String binPath = '/usr/local/psql/bin', String dataPath = '/tmp/pgdata' ) {
-        this.binPath = binPath
-        this.dataPath = dataPath
-    }
-
-    public void initDB() {
-        new File(dataPath).with { f->
-            if (!f.exists()) {
-                println "Initializing db at $dataPath"
-                Process p = "$binPath/initdb --auth=trust -D $dataPath".execute()
-                p.waitForProcessOutput(System.out, System.err)
-            }
-        }
-    }
-
-    public Process runPostgres(String[] environment ) {
-        // -i to enable tcp connections since java needs them anyway
-        println "executing postgres datapath: $dataPath, host: $hostName, port: $port"
-        String exec = "$binPath/postgres -h $hostName -k /tmp -p $port -i -D $dataPath"
-        Process p
-
-        new Thread() {
-            @Override
-            void run() {
-                println exec
-                p = exec.execute(environment, null)
-                p.waitForProcessOutput(System.out, System.err)
-
-            }
-        }.start()
-        while (p == null){
-            Thread.sleep(100)
-        }
-        return p
-    }
-    public void writePgIdent(String text) {
-        Util.appendToFile("$dataPath/pg_ident.conf", text, true)
-    }
-
-    public boolean waitForHBA( int milliseconds ) {
-        long now = System.nanoTime();
-        while ( System.nanoTime() < (now + (milliseconds * 1E6)) ) {
-            if (new File("$dataPath/pg_hba.conf").exists()) {
-                return true
-            }
-        }
-        return false
-    }
-    public void writePgHBA(String text) {
-        Util.appendToFile("$dataPath/pg_hba.conf", text, true)
-    }
-    public String readPgHBA() {
-        Util.readFile("$dataPath/pg_hba.conf")
-    }
-    public void writePgConf(String text) {
-        Util.appendToFile("$dataPath/postgresql.conf", text, false)
-    }
-    public void setKeyTabLocation(String location) {
-        writePgConf("krb_server_keyfile = '$location'")
-    }
-    public void enableGSS(String hostAddress, String mode, String options) {
-        writePgHBA("$mode all all $hostAddress/32 gss map=mymap")
-    }
-    public void enableMyMap(String realm) {
-        writePgIdent("mymap  /^(.*)@$realm\$  \\1")
-    }
-    // enables a database user that is different than the kerberos login user
-    public void enableOwnerMap(String principal, String realm, String user) {
-        writePgIdent("mymap $principal@$realm  $user")
-    }
-
-    public Process startPostgres(String []environment) {
-        port= Util.findPort()
-        // postgres.writePgHBA("host all all $postgres.hostName/32 gss map=mymap")
-        runPostgres(environment)
-    }
-
-    public void reload() {
-        Process p = "$binPath/pg_ctl -D $dataPath reload".execute()
-        p.waitForProcessOutput(System.out, System.err)
-    }
-
-    public static void main( String[] args) {
-        Postgres postgres = new Postgres()
-        postgres.setupPaths('/usr/local/pgsql/12/bin/','/tmp/pgdata11')
-        postgres.initDB()
-        postgres.port= Util.findPort()
-        // postgres.writePgHBA("host all all $postgres.hostName/32 gss map=mymap")
-        Process p = postgres.runPostgres()
-        p.destroy()
-
-        println("Process is ${p.isAlive()?'running':'stopped'}" )
-    }
-}
diff --git a/test-gss/src/main/groovy/TestPostgres.groovy b/test-gss/src/main/groovy/TestPostgres.groovy
deleted file mode 100644
index 0254e2f4c1..0000000000
--- a/test-gss/src/main/groovy/TestPostgres.groovy
+++ /dev/null
@@ -1,142 +0,0 @@
-import org.junit.Assert
-import org.postgresql.PGProperty
-import org.postgresql.jdbc.GSSEncMode
-import org.postgresql.util.KerberosTicket
-
-import javax.security.auth.login.AppConfigurationEntry
-import javax.security.auth.login.Configuration
-import java.sql.Connection
-
[email protected]
-class TestPostgres {
-
-    public static void main(String[] args) {
-        String osName = System.getProperty("os.name").toLowerCase()
-        System.setProperty("sun.security.jgss.native", "true")
-        System.setProperty("javax.security.auth.useSubjectCredsOnly", "false")
-        System.err.println "KRB5CCNAME: ${System.getenv('KRB5CCNAME')}"
-        System.err.println "KRB5_CONFIG: ${System.getenv('KRB5_CONFIG')}"
-        System.err.println "KRB5_KDC_PROFILE: ${System.getenv('KRB5_KDC_PROFILE')}"
-        boolean  isMac = osName.indexOf("mac") >= 0
-        new TestPostgres().testKerberos(isMac)
-
-    }
-
-    public void testKerberos(boolean isMac) {
-        String host='127.0.0.1'
-        String superUser = System.getProperty("user.name");
-        String superPass = 'test'
-        PgJDBC pgJDBC
-
-        Kerberos kerberos = new Kerberos()
-        kerberos.startKerberos()
-
-        /* force the configuration and adjust the values */
-//        Configuration.setConfiguration(new CustomKrbConfig(kerberos.keytab, kerberos.kdcCache));
-        Map environment = System.getenv()
-        // +2 for the kerberos environment
-        String [] currentEnvironment = new String[environment.size() + 3]
-        environment.eachWithIndex {e,i->
-            currentEnvironment[i] = "${e.key}=${e.value}"
-        }
-        currentEnvironment[environment.size()] = kerberos.env[0]
-        currentEnvironment[environment.size() + 1] = kerberos.env[1]
-        currentEnvironment[environment.size() + 2] = kerberos.env[2]
-        Postgres postgres;
-
-        if (isMac)
-            postgres = new Postgres()
-        else
-            postgres = new Postgres('/usr/lib/postgresql/16/bin/', '/tmp/pggss')
-        /*
-        make sure we can connect
-         */
-        postgres.writePgHBA("host    all             all             127.0.0.1/32            trust")
-        if (postgres.waitForHBA(5000) ) {
-            Process p = postgres.startPostgres(currentEnvironment)
-            sleep(2000)
-            pgJDBC = new PgJDBC(host, postgres.getPort());
-            pgJDBC.addProperty(PGProperty.GSS_ENC_MODE, GSSEncMode.DISABLE.value)
-            pgJDBC.createUser(superUser, superPass, 'test1', 'secret1')
-            pgJDBC.createUser(superUser, superPass, 'test2', 'secret2')
-            pgJDBC.createDatabase(superUser, superPass, 'test1', 'test')
-            pgJDBC.createDatabase(superUser, superPass, 'test2', 'test2')
-            postgres.enableGSS('127.0.0.1', 'hostgssenc', 'map=mymap')
-            postgres.enableMyMap('EXAMPLE.COM')
-            postgres.setKeyTabLocation(kerberos.getKeytab())
-            postgres.reload()
-            pgJDBC.addProperty(PGProperty.GSS_ENC_MODE, GSSEncMode.REQUIRE.value)
-            pgJDBC.addProperty(PGProperty.JAAS_LOGIN, true)
-            pgJDBC.addProperty(PGProperty.JAAS_APPLICATION_NAME, "pgjdbc")
-
-            try {
-                Connection connection;
-                try {
-                    connection = pgJDBC.tryConnect('test', 'auth-test-localhost.postgresql.example.com', postgres.getPort(), 'test1', 'secret1')
-                    if (pgJDBC.select(connection, "SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid()")) {
-                        System.err.println 'GSS authenticated and encrypted Connection succeeded'
-                    } else {
-                        Assert.fail 'GSS authenticated and encrypted Connection failed'
-                        System.exit( -1)
-                    }
-                } catch( Exception ex ) {
-                    System.err.println "PG HBA.conf: \n ${postgres.readPgHBA()}"
-                    ex.printStackTrace()
-                } finally {
-                    connection?.close()
-
-                }
-                postgres.enableOwnerMap('test1', 'EXAMPLE.COM', 'test2')
-                postgres.reload()
-                try {
-                    connection = pgJDBC.tryConnect('test2', 'auth-test-localhost.postgresql.example.com', postgres.getPort(), 'test2', 'secret2')
-                    if (pgJDBC.select(connection, "SELECT gss_authenticated AND encrypted from pg_stat_gssapi where pid = pg_backend_pid()")) {
-                        System.err.println 'GSS authenticated and encrypted Connection succeeded'
-                    } else {
-                        Assert.fail 'GSS authenticated and encrypted Connection failed'
-                        System.exit( -1)
-                    }
-                } catch( Exception ex ) {
-                    System.err.println "PG HBA.conf: \n ${postgres.readPgHBA()}"
-                    ex.printStackTrace()
-                } finally {
-                    connection?.close()
-
-                }
-
-                postgres.enableMyMap('EXAMPLE.COM')
-                postgres.enableGSS('127.0.0.1', 'hostnogssenc', 'map=mymap')
-                postgres.reload()
-                pgJDBC.addProperty(PGProperty.GSS_ENC_MODE, GSSEncMode.DISABLE.value)
-
-                try {
-                    connection = pgJDBC.tryConnect('test', 'auth-test-localhost.postgresql.example.com', postgres.getPort(), 'test1', 'secret1')
-
-                    if (pgJDBC.select(connection, "SELECT gss_authenticated AND not encrypted from pg_stat_gssapi where pid = pg_backend_pid()")) {
-                        System.err.println 'GSS authenticated and not encrypted Connection succeeded'
-                    } else {
-                        Assert.fail 'GSS authenticated and not encrypted Connection failed'
-                        System.exit( -1)
-                    }
-                }catch( Exception ex ) {
-                    System.err.println "PG HBA.conf: \n ${postgres.readPgHBA()}"
-                    ex.printStackTrace()
-                    System.exit( -1)
-
-                } finally {
-                    if (!connection) {
-                        System.err.println "PG HBA.conf: \n ${postgres.readPgHBA()}"
-                    }
-                    connection?.close()
-                }
-            } finally {
-                !p.destroy()
-                !kerberos.destroy()
-            }
-        } else {
-            System.err.println("Unable to create pg_hba.conf")
-            System.exit(-1)
-        }
-
-    }
-}
diff --git a/test-gss/src/main/groovy/Util.groovy b/test-gss/src/main/groovy/Util.groovy
deleted file mode 100644
index 4425640779..0000000000
--- a/test-gss/src/main/groovy/Util.groovy
+++ /dev/null
@@ -1,26 +0,0 @@
-import java.net.ServerSocket
-import java.nio.channels.FileChannel;
-
-public class Util {
-    public static int findPort() {
-        int port
-        ServerSocket s = new ServerSocket(0)
-        port = s.getLocalPort()
-        s.close()
-        return port
-    }
-    public static void appendToFile(String fileName, String text, truncate=false) {
-
-        new File(fileName).with() { f ->
-            if ( truncate ) {
-                FileChannel outChannel = new FileOutputStream(f, true).getChannel()
-                outChannel.truncate(0)
-                outChannel.close()
-            }
-            f.append("$text\n ")
-        }
-    }
-    public static String readFile(String fileName) {
-        new FileInputStream(fileName).text
-    }
-}


reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: github://pgjdbc/pgjdbc
  Cc: [email protected], [email protected]
  Subject: Re: [pgjdbc/pgjdbc] PR #4166: refactor(test-gss): convert to Java/JUnit 5 submodule of the main build
  In-Reply-To: <<[email protected]>>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox