diff --git a/.env b/.env
index 842dd115a..cfd55ed55 100644
--- a/.env
+++ b/.env
@@ -1,7 +1,7 @@
VERSION=3.1
TOMCAT_DOCKER_VERSION=11.0-jre17-temurin
-DB_VERSION=10.6.11
+DB_VERSION=12.3
MONGODB_VERSION=4.1.13
IMAGE_TOMCAT=owasp/security-shepherd
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 74e697194..32ae5f18f 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -5,6 +5,17 @@ updates:
schedule:
interval: "weekly"
open-pull-requests-limit: 10
+ ignore:
+ # Its strict Base64 decoder breaks SAML login (see the commons-codec comment in pom.xml)
+ - dependency-name: "commons-codec:commons-codec"
+ versions: ["1.23.0"]
+ # 4.0 turns TLS on by default and the Docker database has it off. Remove once #977 is done.
+ - dependency-name: "org.mariadb.jdbc:mariadb-java-client"
+ update-types: ["version-update:semver-major"]
+ # Stay on the long-term support line (9.7): later majors such as 26.x are short-lived
+ # innovation releases. Patch releases still come through.
+ - dependency-name: "com.mysql:mysql-connector-j"
+ update-types: ["version-update:semver-major"]
- package-ecosystem: github-actions
directory: "/"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b2ed3a359..b06d124ec 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -21,9 +21,9 @@ jobs:
name: build
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up JDK 17
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
+ uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: 'temurin'
java-version: '17'
@@ -38,42 +38,42 @@ jobs:
- name: Build Maven with Docker Profile and Generate SBOM
run: mvn clean install -Pdocker -DskipTests -B -DexcludeTestProject=true cyclonedx:makeBom
- name: Upload WAR File
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: security_shepherd_war
path: target/*.war
- name: Docker Compose Build
run: docker-compose build
- name: Generate Tomcat Docker Image SBOM
- uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
+ uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
id: tomcat
with:
format: cyclonedx-json
image: owasp/security-shepherd
output-file: ${{ github.workspace }}/target/owasp-security-shepherd-tomcat.cyclonedx.json
- name: Generate MariaDB Docker Image SBOM
- uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
+ uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
id: mariadb
with:
format: cyclonedx-json
image: owasp/security-shepherd_mariadb
output-file: ${{ github.workspace }}/target/owasp-security-shepherd-mariadb.cyclonedx.json
- name: Generate MongoDB Docker Image SBOM
- uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
+ uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c # v0.24.3
id: mongodb
with:
format: cyclonedx-json
image: owasp/security-shepherd_mongo
output-file: ${{ github.workspace }}/target/owasp-security-shepherd-mongodb.cyclonedx.json
- name: Upload SBOMs
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: security_shepherd_sboms
path: |
target/*.json
target/*.xml
- name: Cache Maven packages
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
key: maven-output-${{ hashFiles('target/**') }}
path: |
@@ -87,17 +87,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Log in to Docker Hub
- uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
+ uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6.1.0
+ uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: owasp/security-shepherd
# Branch pushes publish :dev / :master. A release tag such as 4.1.0
@@ -109,7 +109,7 @@ jobs:
type=semver,pattern={{major}}
- name: Restore Cached Maven packages
- uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
key: maven-output-${{ hashFiles('target/**') }}
path: |
@@ -124,7 +124,7 @@ jobs:
env-file: .env
- name: Build and push Tomcat
- uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
+ uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
push: true
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index d28b78d44..fe7f7d257 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -25,8 +25,8 @@ jobs:
src: ${{ steps.filter.outputs.src }}
java: ${{ steps.filter.outputs.java }}
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- - uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3
id: filter
with:
filters: |
@@ -44,7 +44,7 @@ jobs:
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: critical
@@ -54,8 +54,8 @@ jobs:
if: needs.detect-changes.outputs.java == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: '17'
@@ -68,8 +68,8 @@ jobs:
if: needs.detect-changes.outputs.src == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: '17'
@@ -88,8 +88,8 @@ jobs:
if: needs.detect-changes.outputs.src == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: '17'
@@ -102,17 +102,18 @@ jobs:
if: needs.detect-changes.outputs.src == 'true'
runs-on: ubuntu-latest
services:
+ # Keep in step with DB_VERSION in .env, the MariaDB the Docker image runs
mysql:
- image: mysql:5.7
+ image: mariadb:12.3
env:
- MYSQL_ROOT_PASSWORD: password
- MYSQL_DATABASE: core
+ MARIADB_ROOT_PASSWORD: password
+ MARIADB_DATABASE: core
ports:
- '8888:3306'
- options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+ options: --health-cmd="healthcheck.sh --connect --innodb_initialized" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0
with:
distribution: temurin
java-version: '17'
diff --git a/.mvn/pgp-keys.map b/.mvn/pgp-keys.map
index ef7086dec..34e4bfbe9 100644
--- a/.mvn/pgp-keys.map
+++ b/.mvn/pgp-keys.map
@@ -13,14 +13,11 @@ net.shibboleth = 0xB5B5DD332142AD657E8D87AC7D27E610B8A3DC52
# review it, then add its groupId here.
com.fasterxml.jackson.core = any
com.fasterxml.jackson.datatype = any
-com.github.ben-manes.caffeine = any
-com.github.waffle = any
com.google.code.findbugs = any
com.google.errorprone = any
com.google.guava = any
com.google.j2objc = any
com.google.protobuf = any
-com.googlecode.json-simple = any
com.mysql = any
com.zaxxer = any
commons-codec = any
@@ -34,7 +31,6 @@ jakarta.activation = any
jakarta.el = any
jakarta.mail = any
jakarta.servlet.jsp.jstl = any
-junit = any
net.bytebuddy = any
net.java.dev.jna = any
net.sf.jtidy = any
@@ -46,14 +42,12 @@ org.apache.santuario = any
org.apache.velocity = any
org.apiguardian = any
org.bouncycastle = any
-org.checkerframework = any
org.cryptacular = any
org.eclipse.angus = any
org.glassfish = any
org.glassfish.web = any
org.hamcrest = any
org.jetbrains = any
-org.jetbrains.kotlin = any
org.json = any
org.jsoup = any
org.jspecify = any
@@ -68,7 +62,3 @@ org.owasp.encoder = any
org.pac4j = any
org.slf4j = any
org.springframework = any
-
-# Unsigned legacy artifact pulled in via json-simple -> junit 4.10 (to be removed, see #944).
-# Exact version only.
-org.hamcrest:hamcrest-core:1.1 = noSig
diff --git a/AGENTS.md b/AGENTS.md
index 8fab41324..4cc54a917 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -61,7 +61,7 @@ docker run -d --name secshep_test_db \
-e MYSQL_ROOT_PASSWORD=CowSaysMoo \
-e MYSQL_DATABASE=core \
-p 3306:3306 \
- mariadb:10.6.11
+ mariadb:12.3
```
The docker-compose `db` service is **not suitable for quick test runs** — it requires `mvn -Pdocker validate` first to generate SQL init scripts, and those scripts run on first container startup to create all challenge schemas.
@@ -217,10 +217,9 @@ pac4j-saml depends on **OpenSAML 5, which is not on Maven Central**. Shibboleth,
- **Repository filtering** (`.mvn/maven.config`, `.mvn/rrf/groupId-shibboleth-releases.txt`): the Shibboleth repository may serve only the `org.opensaml` and `net.shibboleth` groupIds. Everything else comes from Central only.
- This needs **Maven 3.9.2 or newer**: older versions don't expand the path in `.mvn/maven.config` and silently skip the filter. `maven-enforcer-plugin` refuses to build on older Maven.
- The enforcer itself resolves dependencies before it fails, so if an older Maven was ever used on a machine, delete its local repository (`~/.m2/repository`) before building again.
-- **PGP signature verification** (`pgpverify-maven-plugin` in `pom.xml`, rules in `.mvn/pgp-keys.map`): every dependency's signature is checked on every build (with the one unsigned exception below).
+- **PGP signature verification** (`pgpverify-maven-plugin` in `pom.xml`, rules in `.mvn/pgp-keys.map`): every dependency's signature is checked on every build. There are no `noSig` exceptions; don't add one without reviewing why the artifact is unsigned.
- `org.opensaml` / `net.shibboleth` are pinned to the Shibboleth release key `B5B5DD332142AD657E8D87AC7D27E610B8A3DC52`, from https://shibboleth.net/downloads/PGP_KEYS. Verify any new key there before changing the pin.
- Every other groupId is listed by name and needs a valid signature. A new dependency from an unlisted groupId fails with `Not allowed artifact`: review it, then add its groupId.
- - **One exception:** `org.hamcrest:hamcrest-core:1.1` is unsigned and allowed with `noSig` for that exact version only. It arrives through `json-simple` → JUnit 4.10, and is to be removed with #944. Don't add `noSig` rules for anything else without the same scrutiny.
- **Never add a wildcard rule** (`*`, `org.*`, …): any matching rule lets an artifact pass, so a broad rule would silently undo the Shibboleth pin.
- Keys are fetched from `keys.openpgp.org` / `keyserver.ubuntu.com` and cached in the local repository.
- **Test:** `bash src/test/scripts/pgp-verification-test.sh` (run in CI's build job) proves three things on the Maven that runs it: the repository filter is enforced, a tampered OpenSAML jar fails the build, and an OpenSAML artifact signed by another key fails the build.
diff --git a/README.md b/README.md
index 797586259..ca5e3a579 100644
--- a/README.md
+++ b/README.md
@@ -82,6 +82,19 @@ Delete `$BACKUP_DIR` (or move it somewhere safe) once the upgrade is working. On
If you had hand-edited the old `server.xml` or `web.xml` (custom connectors, ports, ciphers and so on), re-apply those edits to the new Tomcat 11 files, using the backup as a reference. Don't copy the old files back.
+##### MariaDB 12.3
+The database image now runs MariaDB 12.3 (long-term support) instead of 10.6, which is no longer supported. The `data` volume is kept. On its first start, `MARIADB_AUTO_UPGRADE=1` in `docker-compose.yml` upgrades the volume's system tables, after taking a backup of them inside the volume. Back up your data before rebuilding anyway, because a volume can't be moved back to 10.6 once 12.3 has opened it. Run this while the old container is still up. The dump holds every user's password hash, so like the `conf` backup above it goes outside the repository and is readable by you only:
+
+```console
+BACKUP_DIR="$HOME/shepherd-db-backup"
+mkdir -p "$BACKUP_DIR" && chmod 700 "$BACKUP_DIR"
+(umask 077 && docker exec secshep_mariadb sh -c \
+ 'export MYSQL_PWD="$MYSQL_ROOT_PASSWORD"; exec mysqldump -uroot --all-databases --routines --events' \
+ > "$BACKUP_DIR/shepherd-db-backup.sql")
+```
+
+Delete `$BACKUP_DIR` (or move it somewhere safe) once the upgrade is working.
+
#### Full Guide
[Docker-Environment-Setup](https://github.com/OWASP/SecurityShepherd/wiki/Docker-Environment-Setup)
diff --git a/docker-compose.yml b/docker-compose.yml
index ac07307a5..53fb88809 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -16,6 +16,8 @@ services:
DOCKER_NETWORK_NAME: ${DOCKER_NETWORK_NAME}
environment:
- MYSQL_ROOT_PASSWORD=${DB_PASS}
+ # Upgrades the system tables of a data volume created by an older MariaDB (e.g. 10.6)
+ - MARIADB_AUTO_UPGRADE=1
ports:
- "${DB_SERVER_MAPPED_HOST}:${DB_PORT_MAPPED_HOST}:3306"
mongo:
diff --git a/docker/mariadb/Dockerfile b/docker/mariadb/Dockerfile
index 67db63d26..c7180842d 100644
--- a/docker/mariadb/Dockerfile
+++ b/docker/mariadb/Dockerfile
@@ -41,4 +41,4 @@ COPY --from=builder /workdir/my.cnf /etc/mysql/
ENTRYPOINT ["docker-entrypoint.sh"]
EXPOSE 3306
-CMD ["mysqld"]
+CMD ["mariadbd"]
diff --git a/pom.xml b/pom.xml
index 256c46c8a..2a1d58ea2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -18,6 +18,8 @@
servlet mocks. -->
7.0.10
6.6.0
+ 2.26.1
+ 6.1.3
${project.basedir}/.mvn/pgp-keys.map
@@ -51,6 +53,21 @@
pom
import
+
+
+ org.apache.logging.log4j
+ log4j-bom
+ ${log4j.version}
+ pom
+ import
+
+
+
+ org.apache.httpcomponents.core5
+ httpcore5
+ 5.4.4
+
@@ -71,30 +88,24 @@
org.apache.logging.log4j
log4j-slf4j2-impl
- 2.25.4
+ ${log4j.version}
de.mkammerer
argon2-jvm
- 2.11
+ 2.12
org.apache.logging.log4j
log4j-core
- 2.25.4
+ ${log4j.version}
org.json
json
- 20260522
-
-
-
- com.googlecode.json-simple
- json-simple
- 1.1.1
+ 20260814
commons-io
@@ -102,17 +113,18 @@
2.22.0
-
+
commons-codec
commons-codec
- 1.21.0
+ 1.22.1
org.jsoup
jsoup
- 1.22.1
+ 1.23.2
@@ -166,28 +178,28 @@
org.mariadb.jdbc
mariadb-java-client
- 3.3.2
+ 3.5.11
com.zaxxer
HikariCP
- 4.0.3
+ 7.1.0
org.owasp.encoder
encoder
- 1.3.1
+ 1.6.0
commons-logging
commons-logging
- 1.2
+ 1.4.0
@@ -225,14 +237,14 @@
org.junit.jupiter
junit-jupiter-api
- 5.11.4
+ ${junit.version}
test
org.junit.jupiter
junit-jupiter-engine
- 5.11.4
+ ${junit.version}
test
@@ -248,22 +260,34 @@
org.slf4j
slf4j-simple
- 1.7.36
+ 2.0.20
test
-
+
org.mockito
mockito-core
- 4.9.0
+ 5.24.0
+ test
-
+
+
+ org.jetbrains
+ annotations
+ 26.1.0
+ provided
+
+
+
io.github.cdimascio
- java-dotenv
- 5.2.2
+ dotenv-java
+ 3.2.0
+ test
@@ -329,7 +353,7 @@
exec-maven-plugin
- 3.6.3
+ 3.6.4
org.codehaus.mojo
@@ -347,7 +371,7 @@
org.codehaus.mojo
keytool-maven-plugin
- 1.7
+ 2.0.2
generate-tls-key
@@ -366,13 +390,6 @@
${TLS_KEYSTORE_PASS}
${STORE_TYPE}
-
-
- org.codehaus.plexus
- plexus-utils
- 4.0.3
-
-
@@ -390,7 +407,7 @@
io.fabric8
docker-maven-plugin
- 0.48.1
+ 0.49.0
start
@@ -493,7 +510,7 @@
org.codehaus.mojo
properties-maven-plugin
- 1.3.0
+ 1.3.1
initialize
@@ -510,7 +527,7 @@
maven-compiler-plugin
- 3.13.0
+ 3.16.0
17
UTF-8
@@ -518,7 +535,7 @@
maven-war-plugin
- 3.3.2
+ 3.5.1
src/main/webapp
@@ -569,7 +586,7 @@
org.apache.maven.plugins
maven-surefire-plugin
- 3.5.2
+ 3.6.0
${skipUTs}
@@ -591,9 +608,8 @@
org.apache.maven.plugins
maven-failsafe-plugin
- 3.5.2
+ 3.6.0
- ${skipTests}
${skipITs}
**/*Test*
@@ -614,7 +630,7 @@
com.diffplug.spotless
spotless-maven-plugin
- 3.6.0
+ 3.10.4
@@ -622,8 +638,9 @@
src/test/java/**/*.java
src/it/java/**/*.java
+
- 1.25.2
+ 1.28.0
@@ -640,7 +657,7 @@
org.codehaus.mojo
build-helper-maven-plugin
- 3.6.1
+ 3.6.2
add-test-source
diff --git a/src/it/java/dbProcs/CoreSchemaCharsetIT.java b/src/it/java/dbProcs/CoreSchemaCharsetIT.java
new file mode 100644
index 000000000..2cfc9d9d8
--- /dev/null
+++ b/src/it/java/dbProcs/CoreSchemaCharsetIT.java
@@ -0,0 +1,94 @@
+package dbProcs;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.io.IOException;
+import java.sql.CallableStatement;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import testUtils.TestProperties;
+
+/**
+ * coreSchema.sql must give core and backup, and every table in them, the same character set and
+ * collation on every server. Left to the server default they changed between MariaDB 10.6 (latin1
+ * for backup) and 12.3 (utf8mb4_uca1400_ai_ci). A table whose collation differs from the schema's
+ * breaks every stored procedure that compares one of its columns with a parameter ("Illegal mix of
+ * collations").
+ */
+public class CoreSchemaCharsetIT {
+
+ private static final Logger log = LogManager.getLogger(CoreSchemaCharsetIT.class);
+
+ @BeforeAll
+ public static void setup() throws IOException, SQLException {
+ TestProperties.setTestPropertiesFileDirectory(log);
+ TestProperties.createMysqlResource();
+ TestProperties.ensureSchemaReady(log);
+ }
+
+ @Test
+ public void coreSchema_IsUtf8mb4GeneralCi() throws IOException, SQLException {
+ assertSchemaCharset("core");
+ }
+
+ @Test
+ public void backupSchema_IsUtf8mb4GeneralCi() throws IOException, SQLException {
+ assertSchemaCharset("backup");
+ }
+
+ @Test
+ public void coreAndBackupTables_AreUtf8mb4GeneralCi() throws IOException, SQLException {
+ try (Connection conn = Database.getDatabaseConnection(null);
+ PreparedStatement ps =
+ conn.prepareStatement(
+ "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_COLLATION FROM information_schema.TABLES"
+ + " WHERE TABLE_SCHEMA IN ('core', 'backup') AND TABLE_TYPE = 'BASE TABLE'");
+ ResultSet rs = ps.executeQuery()) {
+ int tables = 0;
+ while (rs.next()) {
+ tables++;
+ assertEquals(
+ "utf8mb4_general_ci",
+ rs.getString(3),
+ "collation of " + rs.getString(1) + "." + rs.getString(2));
+ }
+ assertTrue(tables > 0, "no tables found in core or backup");
+ }
+ }
+
+ @Test
+ public void procedureComparingColumnWithParameter_Runs() throws IOException, SQLException {
+ // Other ITs reseed core.users, so don't rely on any row: the collation check happens when the
+ // statement runs, whether or not a row matches
+ try (Connection conn = Database.getDatabaseConnection(null);
+ CallableStatement cs = conn.prepareCall("CALL core.userFind(?)")) {
+ cs.setString(1, "noSuchUser");
+ try (ResultSet rs = cs.executeQuery()) {
+ assertFalse(rs.next(), "userFind found a user that does not exist");
+ }
+ }
+ }
+
+ private static void assertSchemaCharset(String schema) throws IOException, SQLException {
+ try (Connection conn = Database.getDatabaseConnection(null);
+ PreparedStatement ps =
+ conn.prepareStatement(
+ "SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME"
+ + " FROM information_schema.SCHEMATA WHERE SCHEMA_NAME = ?")) {
+ ps.setString(1, schema);
+ try (ResultSet rs = ps.executeQuery()) {
+ assertTrue(rs.next(), "schema " + schema + " not found");
+ assertEquals("utf8mb4", rs.getString(1));
+ assertEquals("utf8mb4_general_ci", rs.getString(2));
+ }
+ }
+ }
+}
diff --git a/src/main/resources/database/coreSchema.sql b/src/main/resources/database/coreSchema.sql
index 12b74f0c1..98450823e 100644
--- a/src/main/resources/database/coreSchema.sql
+++ b/src/main/resources/database/coreSchema.sql
@@ -6,7 +6,11 @@ SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL';
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP SCHEMA IF EXISTS `core` ;
-CREATE SCHEMA IF NOT EXISTS `core` DEFAULT CHARACTER SET utf8mb4 ;
+-- Collation named explicitly, here and on every table: MariaDB 11.5+ defaults utf8mb4 to
+-- utf8mb4_uca1400_ai_ci, which compares and sorts differently from the utf8mb4_general_ci core has
+-- always had. A table that differs from the schema breaks the stored procedures ("Illegal mix of
+-- collations"), because their parameters take the schema's collation.
+CREATE SCHEMA IF NOT EXISTS `core` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ;
USE `core` ;
SELECT "Creating Tables" FROM DUAL;
@@ -20,7 +24,7 @@ CREATE TABLE IF NOT EXISTS `core`.`class` (
`classYear` VARCHAR(5) NOT NULL ,
PRIMARY KEY (`classId`) )
ENGINE = InnoDB
-DEFAULT CHARACTER SET = utf8mb4;
+DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
-- -----------------------------------------------------
-- Table `core`.`users`
@@ -53,7 +57,7 @@ CREATE TABLE IF NOT EXISTS `core`.`users` (
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB
-DEFAULT CHARACTER SET = utf8mb4;
+DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
-- -----------------------------------------------------
-- Table `core`.`modules`
@@ -74,7 +78,7 @@ CREATE TABLE IF NOT EXISTS `core`.`modules` (
`isUnsafe` TINYINT(1) NOT NULL DEFAULT TRUE,
PRIMARY KEY (`moduleId`) )
ENGINE = InnoDB
-DEFAULT CHARACTER SET = utf8mb4;
+DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
-- -----------------------------------------------------
-- Table `core`.`medals`
@@ -100,7 +104,7 @@ CREATE TABLE IF NOT EXISTS `core`.`medals` (
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB
-DEFAULT CHARACTER SET = utf8mb4;
+DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
-- -----------------------------------------------------
-- Table `core`.`results`
@@ -130,7 +134,7 @@ CREATE TABLE IF NOT EXISTS `core`.`results` (
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB
-DEFAULT CHARACTER SET = utf8mb4;
+DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
-- -----------------------------------------------------
-- Table `core`.`cheatsheet`
@@ -148,7 +152,7 @@ CREATE TABLE IF NOT EXISTS `core`.`cheatsheet` (
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB
-DEFAULT CHARACTER SET = utf8mb4;
+DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
-- -----------------------------------------------------
-- Table `core`.`sequence`
@@ -158,7 +162,7 @@ CREATE TABLE IF NOT EXISTS `core`.`sequence` (
`currVal` BIGINT(20) NOT NULL DEFAULT 282475249 ,
PRIMARY KEY (`tableName`) )
ENGINE = InnoDB
-DEFAULT CHARACTER SET = utf8mb4;
+DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
-- -----------------------------------------------------
-- Table `core`.`settings`
@@ -168,7 +172,7 @@ CREATE TABLE IF NOT EXISTS `core`.`settings` (
`value` VARCHAR(64) NOT NULL ,
PRIMARY KEY (`setting`) )
ENGINE = InnoDB
-DEFAULT CHARACTER SET = utf8mb4;
+DEFAULT CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci;
SELECT "Creating Procedures" FROM DUAL;
@@ -1749,7 +1753,9 @@ call userCreate(null, 'admin', '$argon2i$v=19$m=65536,t=10,p=1$7oxgR8QkdOd4tsHFi
SELECT "Creating BackUp Schema" FROM DUAL;
DROP DATABASE IF EXISTS backup;
-CREATE DATABASE backup;
+-- Same character set as core, whose tables it copies; it used to take the server default (latin1 on
+-- MariaDB 10.6)
+CREATE DATABASE backup DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
SET GLOBAL event_scheduler = ON;
SET @@global.event_scheduler = ON;
diff --git a/src/test/java/testUtils/HamcrestClasspathTest.java b/src/test/java/testUtils/HamcrestClasspathTest.java
new file mode 100644
index 000000000..355212dbe
--- /dev/null
+++ b/src/test/java/testUtils/HamcrestClasspathTest.java
@@ -0,0 +1,25 @@
+package testUtils;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Guards the test classpath against the Hamcrest 1.1 that the unused json-simple dependency brought
+ * in through JUnit 4.10 (#944). Its {@code Matcher} shadowed Hamcrest 3's, so a failing {@code
+ * assertThat} threw {@code NoSuchMethodError} instead of reporting the mismatch.
+ */
+class HamcrestClasspathTest {
+
+ @Test
+ void failingAssertThat_ReportsExpectedAndActual() {
+ AssertionError error =
+ assertThrows(AssertionError.class, () -> assertThat("actual", equalTo("expected")));
+
+ assertTrue(error.getMessage().contains("Expected: \"expected\""), error.getMessage());
+ assertTrue(error.getMessage().contains("but: was \"actual\""), error.getMessage());
+ }
+}