Message-ID: From: "vlsi (@vlsi)" To: "pgjdbc/pgjdbc" Date: Sun, 14 Jun 2026 14:22:36 +0000 Subject: [pgjdbc/pgjdbc] PR #4166: refactor(test-gss): convert to Java/JUnit 5 submodule of the main build List-Id: X-GitHub-Additions: 652 X-GitHub-Author-Id: 213894 X-GitHub-Author-Login: vlsi X-GitHub-Base: master X-GitHub-Changed-Files: 21 X-GitHub-Commits: 2 X-GitHub-Deletions: 1005 X-GitHub-Head-Branch: claude/wonderful-albattani-79d05c X-GitHub-Head-SHA: 98d00e8b69fcecb208aaf585d912a24846d80e44 X-GitHub-Issue: 4166 X-GitHub-Labels: building-and-testing X-GitHub-Merge-SHA: 479826611b749a0b27a0720267bc94828222f6ed X-GitHub-Repo: pgjdbc/pgjdbc X-GitHub-Requested-Reviewers: davecramer X-GitHub-State: open X-GitHub-Type: pull_request X-GitHub-Url: https://github.com/pgjdbc/pgjdbc/pull/4166 Content-Type: text/plain; charset=utf-8 ## 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. + * + *

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 command, Map 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 command, Map 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/} 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 env = new LinkedHashMap<>(); + private Process krb5Process; + + String getKeytab() { + return keytab; + } + + Map 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 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-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# 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 @@ -@groovy.transform.CompileStatic -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 - -@groovy.transform.CompileStatic -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 @@ -@groovy.transform.CompileStatic -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 - -@groovy.transform.CompileStatic -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 - } -}