diff --git a/app/proprietary/build.gradle b/app/proprietary/build.gradle index 0f83a64fbb..394779a8b0 100644 --- a/app/proprietary/build.gradle +++ b/app/proprietary/build.gradle @@ -7,6 +7,10 @@ configurations { h2Migration } +def h2RuntimeVersion = '2.4.240' +def h2MigrationVersion = '2.3.232' +def h2VersionsLockFile = rootProject.file('gradle/h2-versions.lock') + bootRun { enabled = false } @@ -62,8 +66,8 @@ dependencies { api "io.jsonwebtoken:jjwt-api:${jwtVersion}" runtimeOnly "io.jsonwebtoken:jjwt-impl:${jwtVersion}" runtimeOnly "io.jsonwebtoken:jjwt-jackson:${jwtVersion}" - runtimeOnly 'com.h2database:h2:2.4.240' // Don't upgrade h2database - h2Migration 'com.h2database:h2:2.3.232' + runtimeOnly "com.h2database:h2:${h2RuntimeVersion}" + h2Migration "com.h2database:h2:${h2MigrationVersion}" runtimeOnly 'org.postgresql:postgresql:42.7.11' implementation('com.coveo:saml-client:5.0.0') { exclude group: 'org.opensaml', module: 'opensaml-core' @@ -91,6 +95,63 @@ tasks.named('processResources') { } } +tasks.named('processTestResources') { + from(h2VersionsLockFile) +} + +tasks.register('verifyH2VersionsLocked') { + group = 'verification' + description = 'Verifies that the resolved H2 versions match the reviewed compatibility lock.' + inputs.file(h2VersionsLockFile) + inputs.property('runtimeVersion', h2RuntimeVersion) + inputs.property('migrationVersion', h2MigrationVersion) + + doLast { + if (!h2VersionsLockFile.isFile()) { + throw new GradleException("Missing H2 compatibility lock: ${h2VersionsLockFile}") + } + + Map lockedVersions = h2VersionsLockFile.readLines('UTF-8') + .findAll { line -> !line.isBlank() && !line.startsWith('#') } + .collectEntries { line -> + def parts = line.split('=', 2) + if (parts.length != 2 || parts[0].isBlank() || parts[1].isBlank()) { + throw new GradleException("Invalid H2 compatibility lock entry: ${line}") + } + [(parts[0]): parts[1]] + } + + Map declaredVersions = [ + runtime: h2RuntimeVersion, + migration: h2MigrationVersion, + ] + if (lockedVersions != declaredVersions) { + throw new GradleException( + "H2 dependency versions changed without updating ${h2VersionsLockFile}. " + + "Run the H2 migration tests and explicitly review the compatibility lock. " + + "Expected ${lockedVersions}, declared ${declaredVersions}.") + } + + Map resolvedVersions = [ + runtime: configurations.runtimeClasspath.resolvedConfiguration.resolvedArtifacts + .find { artifact -> artifact.moduleVersion.id.group == 'com.h2database' && artifact.name == 'h2' } + ?.moduleVersion?.id?.version, + migration: configurations.h2Migration.resolvedConfiguration.resolvedArtifacts + .find { artifact -> artifact.moduleVersion.id.group == 'com.h2database' && artifact.name == 'h2' } + ?.moduleVersion?.id?.version, + ] + if (resolvedVersions != declaredVersions) { + throw new GradleException( + "Resolved H2 versions do not match the reviewed compatibility lock. " + + "Expected ${declaredVersions}, resolved ${resolvedVersions}.") + } + } +} + +tasks.named('check') { + dependsOn(tasks.named('verifyH2VersionsLocked')) +} + tasks.register('prepareKotlinBuildScriptModel') {} tasks.register('type3SignatureTool', JavaExec) { diff --git a/app/proprietary/src/test/java/stirling/software/proprietary/security/migration/H2VersionCompatibilityTest.java b/app/proprietary/src/test/java/stirling/software/proprietary/security/migration/H2VersionCompatibilityTest.java new file mode 100644 index 0000000000..72bd8332b0 --- /dev/null +++ b/app/proprietary/src/test/java/stirling/software/proprietary/security/migration/H2VersionCompatibilityTest.java @@ -0,0 +1,64 @@ +package stirling.software.proprietary.security.migration; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.io.InputStream; +import java.sql.DriverManager; +import java.util.Properties; +import java.util.jar.Attributes; +import java.util.jar.JarInputStream; + +import org.junit.jupiter.api.Test; + +class H2VersionCompatibilityTest { + + private static final String H2_VERSIONS_LOCK_RESOURCE = "h2-versions.lock"; + private static final String OLD_H2_RESOURCE = "h2-migration/h2-2.3.232.jar"; + + @Test + void runtimeAndMigrationDriverMatchTheReviewedCompatibilityLock() throws Exception { + Properties lockedVersions = readLockedVersions(); + + assertThat(runtimeH2Version()).isEqualTo(lockedVersions.getProperty("runtime")); + assertThat(migrationDriverVersion()).isEqualTo(lockedVersions.getProperty("migration")); + } + + private Properties readLockedVersions() throws IOException { + try (InputStream resource = + H2VersionCompatibilityTest.class + .getClassLoader() + .getResourceAsStream(H2_VERSIONS_LOCK_RESOURCE)) { + assertThat(resource).as("H2 compatibility lock resource").isNotNull(); + Properties versions = new Properties(); + versions.load(resource); + assertThat(versions).containsKeys("runtime", "migration"); + return versions; + } + } + + private String runtimeH2Version() throws Exception { + try (var connection = DriverManager.getConnection("jdbc:h2:mem:h2VersionCompatibility")) { + try (var statement = connection.prepareStatement("SELECT H2VERSION() FROM DUAL")) { + try (var result = statement.executeQuery()) { + assertThat(result.next()).isTrue(); + return result.getString(1); + } + } + } + } + + private String migrationDriverVersion() throws IOException { + InputStream resource = + H2VersionCompatibilityTest.class + .getClassLoader() + .getResourceAsStream(OLD_H2_RESOURCE); + assertThat(resource).as("bundled H2 migration driver").isNotNull(); + try (resource; + JarInputStream jar = new JarInputStream(resource)) { + return jar.getManifest() + .getMainAttributes() + .getValue(Attributes.Name.IMPLEMENTATION_VERSION); + } + } +} diff --git a/gradle/h2-versions.lock b/gradle/h2-versions.lock new file mode 100644 index 0000000000..06b85e2f4e --- /dev/null +++ b/gradle/h2-versions.lock @@ -0,0 +1,3 @@ +# H2 versions reviewed with the migration and restore-safety tests. +runtime=2.4.240 +migration=2.3.232