pgjdbc/pgjdbc GitHub issues and pull requests (mirror)
help / color / mirror / Atom feedFrom: 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