diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java index 1ff103456..b69e38717 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDao.java @@ -83,6 +83,7 @@ public class JdbcDao implements DonkeyDao { private String statsServerId; private boolean transactionAlteredChannels = false; private char quoteChar = '"'; + private boolean scopeTablesToCatalog = false; private Logger logger = LogManager.getLogger(this.getClass()); protected JdbcDao(Donkey donkey, Connection connection, QuerySource querySource, SerializerProvider serializerProvider, boolean encryptMessageContent, boolean encryptAttachments, boolean encryptCustomMetaData, boolean decryptData, StatisticsUpdater statisticsUpdater, Statistics currentStats, Statistics totalStats, String statsServerId) { @@ -130,6 +131,20 @@ public void setQuoteChar(char quoteChar) { this.quoteChar = quoteChar; } + /** + * Limits table lookups to the connection's own catalog. MySQL Connector/J treats a null catalog + * as every database the login can see, so without this a second database on a shared login + * matches the first one's tables. Off by default; enabled by {@link MysqlDaoFactory}. See + * OpenIntegrationEngine/engine#489. + */ + public void setScopeTablesToCatalog(boolean scopeTablesToCatalog) { + this.scopeTablesToCatalog = scopeTablesToCatalog; + } + + private String getTableCatalog() throws SQLException { + return scopeTablesToCatalog ? connection.getCatalog() : null; + } + @Override public void insertMessage(Message message) { logger.debug(message.getChannelId() + "/" + message.getMessageId() + ": inserting message"); @@ -1924,7 +1939,7 @@ public void checkAndCreateChannelTables() { try { DatabaseMetaData dbMetaData = connection.getMetaData(); - try (ResultSet rs = dbMetaData.getTables(null, null, "%", null)) { + try (ResultSet rs = dbMetaData.getTables(getTableCatalog(), null, "%", null)) { while (rs.next()) { String tableName = rs.getString("TABLE_NAME").toLowerCase(); channelTablesMap.remove(tableName); @@ -2238,10 +2253,11 @@ public boolean initTableStructure() { private boolean tableExists(String tableName) { try { DatabaseMetaData metaData = connection.getMetaData(); + String catalog = getTableCatalog(); // Databases disagree on identifier case folding, so try both spellings for (String name : new String[] { tableName, tableName.toUpperCase() }) { - try (ResultSet resultSet = metaData.getTables(null, null, name, null)) { + try (ResultSet resultSet = metaData.getTables(catalog, null, name, null)) { if (resultSet.next()) { return true; } diff --git a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java index ab08ebf45..7a888c832 100644 --- a/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java +++ b/donkey/src/main/java/com/mirth/connect/donkey/server/data/jdbc/MysqlDaoFactory.java @@ -16,6 +16,7 @@ public class MysqlDaoFactory extends JdbcDaoFactory { public JdbcDao getDao(SerializerProvider serializerProvider) { JdbcDao dao = super.getDao(serializerProvider); dao.setQuoteChar('`'); + dao.setScopeTablesToCatalog(true); return dao; } } diff --git a/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoTest.java b/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoTest.java index 4cf983e55..5ac513d43 100644 --- a/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoTest.java +++ b/donkey/src/test/java/com/mirth/connect/donkey/server/data/jdbc/JdbcDaoTest.java @@ -10,10 +10,12 @@ package com.mirth.connect.donkey.server.data.jdbc; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.inOrder; @@ -476,4 +478,39 @@ private void setLocalChannelId(String channelId, long localChannelId) { channels.put(channelId, channel); when(donkey.getDeployedChannels()).thenReturn(channels); } + + /** + * Table lookups stay unscoped (null catalog) unless the factory enables catalog scoping, so + * only MySQL changes behaviour. See OpenIntegrationEngine/engine#489. + */ + @Test + public void testTableLookupUnscopedByDefault() throws SQLException { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + ResultSet resultSet = mock(ResultSet.class); + when(connection.getMetaData()).thenReturn(metaData); + when(connection.getCatalog()).thenReturn("oie_b"); + when(metaData.getTables(any(), any(), any(), any())).thenReturn(resultSet); + when(resultSet.next()).thenReturn(true); + + assertFalse(dao.initTableStructure()); + + verify(metaData).getTables(isNull(), isNull(), eq("d_channels"), isNull()); + verify(connection, never()).getCatalog(); + } + + /** With catalog scoping enabled (MySQL), lookups are limited to the connection's database. */ + @Test + public void testTableLookupScopedToCatalogWhenEnabled() throws SQLException { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + ResultSet resultSet = mock(ResultSet.class); + when(connection.getMetaData()).thenReturn(metaData); + when(connection.getCatalog()).thenReturn("oie_b"); + when(metaData.getTables(any(), any(), any(), any())).thenReturn(resultSet); + when(resultSet.next()).thenReturn(true); + + dao.setScopeTablesToCatalog(true); + assertFalse(dao.initTableStructure()); + + verify(metaData).getTables(eq("oie_b"), isNull(), eq("d_channels"), isNull()); + } } diff --git a/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java b/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java index b4f815d9e..b0b5855f1 100644 --- a/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java +++ b/server/src/main/java/com/mirth/connect/server/util/DatabaseUtil.java @@ -211,14 +211,18 @@ public static boolean tableExists(Connection connection, String tableName) { try { DatabaseMetaData metaData = connection.getMetaData(); - resultSet = metaData.getTables(null, null, tableName.toUpperCase(), new String[] { + // MySQL treats a null catalog as every database the login can see, so scope the lookup + // to this connection's database there. Other databases keep the unscoped lookup. See + // OpenIntegrationEngine/engine#489. + String catalog = isMySql(metaData) ? connection.getCatalog() : null; + resultSet = metaData.getTables(catalog, null, tableName.toUpperCase(), new String[] { "TABLE" }); if (resultSet.next()) { return true; } - resultSet = metaData.getTables(null, null, tableName.toLowerCase(), new String[] { + resultSet = metaData.getTables(catalog, null, tableName.toLowerCase(), new String[] { "TABLE" }); return resultSet.next(); } catch (SQLException e) { @@ -228,6 +232,11 @@ public static boolean tableExists(Connection connection, String tableName) { } } + private static boolean isMySql(DatabaseMetaData metaData) throws SQLException { + String productName = metaData.getDatabaseProductName(); + return "MySQL".equalsIgnoreCase(productName) || "MariaDB".equalsIgnoreCase(productName); + } + /** * Tell whether or not the given index exists in the database */ diff --git a/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java b/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java new file mode 100644 index 000000000..3029cdd27 --- /dev/null +++ b/server/src/test/java/com/mirth/connect/server/util/DatabaseUtilTest.java @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: Open Integration Engine Contributors + +package com.mirth.connect.server.util; + +import static org.junit.Assert.assertFalse; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.ResultSet; + +import org.junit.Test; + +public class DatabaseUtilTest { + + private static Connection connectionFor(String productName, DatabaseMetaData metaData) throws Exception { + Connection connection = mock(Connection.class); + ResultSet resultSet = mock(ResultSet.class); + + when(connection.getMetaData()).thenReturn(metaData); + when(connection.getCatalog()).thenReturn("oie_b"); + when(metaData.getDatabaseProductName()).thenReturn(productName); + when(metaData.getTables(any(), any(), any(), any())).thenReturn(resultSet); + when(resultSet.next()).thenReturn(false); + return connection; + } + + /** + * On MySQL, tableExists must scope its metadata lookup to the connection's own catalog. A null + * catalog lets Connector/J match tables in any database the login can see, so a second OIE + * database on a shared login is never migrated. See OpenIntegrationEngine/engine#489. + */ + @Test + public void tableExists_onMySql_scopesLookupToConnectionCatalog() throws Exception { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + Connection connection = connectionFor("MySQL", metaData); + + assertFalse(DatabaseUtil.tableExists(connection, "CONFIGURATION")); + + verify(metaData).getTables(eq("oie_b"), isNull(), eq("CONFIGURATION"), any()); + verify(metaData).getTables(eq("oie_b"), isNull(), eq("configuration"), any()); + } + + /** Other databases keep the existing unscoped (null catalog) lookup. */ + @Test + public void tableExists_onOtherDatabases_keepsNullCatalog() throws Exception { + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + Connection connection = connectionFor("PostgreSQL", metaData); + + assertFalse(DatabaseUtil.tableExists(connection, "CONFIGURATION")); + + verify(metaData).getTables(isNull(), isNull(), eq("CONFIGURATION"), any()); + verify(metaData).getTables(isNull(), isNull(), eq("configuration"), any()); + verify(connection, never()).getCatalog(); + } +}