From ea06fd66806102fb9790b336afc9770091de3767 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Sun, 27 Sep 2026 08:56:36 +0000
Subject: [PATCH] [#1075] Test the JDBC guards against a neighbouring MySQL database and Oracle schema (#1108)
---
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/OracleTestCase.java | 129 ++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 126 insertions(+), 3 deletions(-)
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/OracleTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/OracleTestCase.java
index 0b9547d..69b923a 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/OracleTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/OracleTestCase.java
@@ -11,17 +11,32 @@
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions Copyright [year] [name of copyright owner]".
*
- * Copyright 2025 3A Systems, LLC.
+ * Copyright 2025-2026 3A Systems, LLC.
*/
package org.opends.server.backends.jdbc;
+import org.forgerock.opendj.ldap.ByteString;
+import org.opends.server.backends.pluggable.spi.AccessMode;
+import org.opends.server.backends.pluggable.spi.TreeName;
+import org.opends.server.backends.pluggable.spi.WriteOperation;
+import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.testcontainers.containers.JdbcDatabaseContainer;
import org.testcontainers.oracle.OracleContainer;
import org.testng.annotations.Test;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
import java.time.Duration;
-//docker run --rm --name oracle-db -p 1521:1521 -e APP_USER=opendj -e ORACLE_DATABASE=database_name -e APP_USER_PASSWORD=password gvenzl/oracle-free:23.26.2-slim-faststart
+import static org.testng.Assert.assertEquals;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.assertTrue;
+
+//docker run --rm --name oracle-db -p 1521:1521 -e APP_USER=opendj -e ORACLE_DATABASE=database_name -e APP_USER_PASSWORD=password -e ORACLE_PASSWORD=password gvenzl/oracle-free:23.26.2-slim-faststart
@Test(sequential = true)
public class OracleTestCase extends TestCase {
@@ -39,7 +54,7 @@
@Override
protected String getContainerDockerCommand() {
- return "run before test: docker run --rm --name oracle-db -p 1521:1521 -e APP_USER=opendj -e ORACLE_DATABASE=database_name -e APP_USER_PASSWORD=password gvenzl/oracle-free:23.26.2-slim-faststart";
+ return "run before test: docker run --rm --name oracle-db -p 1521:1521 -e APP_USER=opendj -e ORACLE_DATABASE=database_name -e APP_USER_PASSWORD=password -e ORACLE_PASSWORD=password gvenzl/oracle-free:23.26.2-slim-faststart";
}
@Override
@@ -52,6 +67,114 @@
return "jdbc:oracle:thin:opendj/password@localhost: " + ((container==null)?"1521":container.getMappedPort(1521)) + "/database_name";
}
+ /** The schema of the case below: another user of this database, holding a directory of its own. */
+ private static final String NEIGHBOUR = "opendj_neighbour1075";
+
+ /**
+ * The administrator of the database, who makes the neighbour and grants this suite a look at it: the
+ * container is given one password for both accounts, and the user of the suite may create no other.
+ */
+ private String getSystemJdbcUrl() {
+ return getJdbcUrl().replace("opendj/password", "system/password");
+ }
+
+ @Override
+ protected void dropStaleNeighbours() throws SQLException {
+ administer("drop user if exists " + NEIGHBOUR + " cascade");
+ }
+
+ /**
+ * A table and an index of another schema answer for none of this backend's (#1075) - the oracle twin
+ * of the postgres case of #902.
+ * <p>
+ * A table is named after its tree and an index after its table, so two directories in two schemas of
+ * one database hold the same table and the same index. Oracle names no catalog, so a lookup of
+ * {@code openTree()} is asked of every schema the user may see, and the schema {@code TableScope}
+ * reads off the connection is the one thing telling the neighbour's index from this one's: found
+ * abroad, the table guard leaves every statement of the backend addressing a table that is not there,
+ * and the index one leaves the {@code where k>? order by k} batches of every cursor a full scan.
+ * <p>
+ * The grant is the whole of the fixture: the dictionary views the driver reads list another user's
+ * table only to a user granted something on it, and without it the lookup finds nothing either way.
+ */
+ @Test
+ public void testAnOpenIsAnsweredForByNoTableOfAnotherSchema() throws Exception {
+ final TreeName tree = new TreeName("testAnotherSchema", "tree");
+ final JDBCStorage storage = new JDBCStorage(createBackendCfg(getBackendId() + "_anotherSchema"), null);
+ final String tableName = storage.getTableName(tree);
+ final String indexName = "k_" + tableName.substring("opendj_".length());
+ try {
+ // the neighbouring directory: the same table and the same index, in a schema this storage
+ // reaches through no unqualified name of its own. Spelled out rather than opened by a storage,
+ // so that the fixture is the collision and nothing else
+ administer("drop user if exists " + NEIGHBOUR + " cascade",
+ "create user " + NEIGHBOUR + " identified by password quota unlimited on users",
+ "create table " + NEIGHBOUR + "." + tableName + " (h char(128),k raw(2000),v blob,primary key(h,k))",
+ "create index " + NEIGHBOUR + "." + indexName + " on " + NEIGHBOUR + "." + tableName + " (k)",
+ "grant select on " + NEIGHBOUR + "." + tableName + " to opendj");
+ assertFalse(isExistsOwn("select 1 from user_tables where table_name=upper(?)", tableName),
+ "the case did not start with the table of this backend absent from its schema");
+
+ storage.open(AccessMode.READ_WRITE);
+ storage.write(new WriteOperation() {
+ @Override
+ public void run(WriteableTransaction txn) throws Exception {
+ txn.openTree(tree, true);
+ // the destructive half of the table guard, and the reason it is loud: found abroad, the
+ // table is created nowhere and this statement addresses a table that is not there
+ txn.put(tree, ByteString.valueOfUtf8("a key of this backend"),
+ ByteString.valueOfUtf8("a value of this backend"));
+ }
+ });
+
+ assertTrue(isExistsOwn("select 1 from user_tables where table_name=upper(?)", tableName),
+ "the open took the table of another schema for its own and created none");
+ assertTrue(isExistsOwn("select 1 from user_indexes where index_name=upper(?)", indexName),
+ "the open took the index of another schema for its own: the cursor batches of this tree are full scans behind it");
+ assertEquals(rowCount(tableName), 1,
+ "the write of this backend landed in a table other than the one the open made");
+ assertEquals(rowCount(NEIGHBOUR + "." + tableName), 0,
+ "the write of this backend landed in the table of the neighbouring schema");
+ } finally {
+ clearQuietly(storage);
+ administer("drop user if exists " + NEIGHBOUR + " cascade");
+ }
+ }
+
+ /** Runs the given statements as the administrator of the database. */
+ private void administer(String... statements) throws SQLException {
+ try (final Connection con = DriverManager.getConnection(getSystemJdbcUrl());
+ final Statement st = con.createStatement()) {
+ for (final String statement : statements) {
+ st.execute(statement);
+ }
+ }
+ }
+
+ /**
+ * Whether the dictionary of the user of this suite lists the named object, asked by name rather than
+ * through the catalog lookups under test.
+ */
+ private boolean isExistsOwn(String query, String name) throws SQLException {
+ try (final Connection con = DriverManager.getConnection(getJdbcUrl());
+ final PreparedStatement st = con.prepareStatement(query)) {
+ st.setString(1, name);
+ try (final ResultSet rs = st.executeQuery()) {
+ return rs.next();
+ }
+ }
+ }
+
+ /** What the named table holds, read as the administrator, who reaches both schemas. */
+ private int rowCount(String qualified) throws SQLException {
+ final String table = qualified.contains(".") ? qualified : "opendj." + qualified;
+ try (final Connection con = DriverManager.getConnection(getSystemJdbcUrl());
+ final Statement st = con.createStatement();
+ final ResultSet rs = st.executeQuery("select count(*) from " + table)) {
+ return rs.next() ? rs.getInt(1) : -1;
+ }
+ }
+
@Override
@Test(skipFailedInvocations = true) //ORA UPSERT error
public void test_issue_496_2() {
--
Gitblit v1.10.0