package com.ruoyi.shadow.startup; import com.ruoyi.shadow.config.ShadowProperties; import org.junit.jupiter.api.Test; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; import java.sql.Statement; import java.util.Map; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class SqlServerShadowDatabaseGateTest { @Test void actualConnectionMetadataCannotResolveToTheSameDatabase() throws Exception { Connection source = connection( "jdbc:sqlserver://prod:1433;databaseName=FT546-wcs", "shadow_ro"); Connection evidence = connection( "jdbc:sqlserver://PROD:1433;databaseName=ft546-WCS", "shadow_evidence"); SqlServerPermissionProbe probe = mock(SqlServerPermissionProbe.class); JdbcTemplate evidenceJdbc = mock(JdbcTemplate.class); SqlServerShadowDatabaseGate gate = new SqlServerShadowDatabaseGate( dataSource(source), dataSource(evidence), evidenceJdbc, probe); IllegalStateException failure = assertThrows( IllegalStateException.class, () -> gate.verify(properties())); assertEquals("SOURCE_AND_EVIDENCE_DATABASE_MUST_DIFFER", failure.getMessage()); verify(probe, never()).verifySource(any(Connection.class)); } @Test void differentJdbcAliasesCannotHideTheSamePhysicalDatabase() throws Exception { Connection source = connection( "jdbc:sqlserver://prod-alias-a:1433;" + "databaseName=FT546-wcs", "shadow_ro", "FT546-SQL\\WCS", "FT546-wcs"); Connection evidence = connection( "jdbc:sqlserver://prod-alias-b:1433;" + "databaseName=FT546-wcs", "shadow_evidence", "FT546-SQL\\WCS", "FT546-wcs"); SqlServerPermissionProbe probe = mock(SqlServerPermissionProbe.class); SqlServerShadowDatabaseGate gate = new SqlServerShadowDatabaseGate( dataSource(source), dataSource(evidence), mock(JdbcTemplate.class), probe); ShadowProperties properties = properties(); properties.getSource().setJdbcUrl( "jdbc:sqlserver://prod-alias-a:1433;" + "databaseName=FT546-wcs"); properties.getEvidence().setJdbcUrl( "jdbc:sqlserver://prod-alias-b:1433;" + "databaseName=FT546-wcs"); IllegalStateException failure = assertThrows( IllegalStateException.class, () -> gate.verify(properties)); assertEquals("SOURCE_AND_EVIDENCE_DATABASE_MUST_DIFFER", failure.getMessage()); verify(probe, never()).verifySource(any(Connection.class)); } @Test void evidenceLoginCannotAccessSourceDatabaseOnTheSameServer() throws Exception { Connection source = connection( "jdbc:sqlserver://prod:1433;databaseName=FT546-wcs", "shadow_ro", "FT546-SQL", "FT546-wcs", 0); Connection evidence = connection( "jdbc:sqlserver://prod:1433;databaseName=FT546-shadow", "shadow_evidence", "FT546-SQL", "FT546-shadow", 1); SqlServerPermissionProbe probe = mock(SqlServerPermissionProbe.class); SqlServerShadowDatabaseGate gate = new SqlServerShadowDatabaseGate( dataSource(source), dataSource(evidence), mock(JdbcTemplate.class), probe); ShadowProperties properties = properties(); properties.getEvidence().setJdbcUrl( "jdbc:sqlserver://prod:1433;databaseName=FT546-shadow"); IllegalStateException failure = assertThrows( IllegalStateException.class, () -> gate.verify(properties)); assertEquals("EVIDENCE_LOGIN_CAN_ACCESS_SOURCE_DATABASE", failure.getMessage()); verify(probe, never()).verifySource(any(Connection.class)); } @Test void rejectedSourcePermissionFailsStartupWithoutEvidenceWrites() throws Exception { Connection source = connection( "jdbc:sqlserver://prod:1433;databaseName=FT546-wcs", "shadow_ro"); Connection evidence = connection( "jdbc:sqlserver://evidence:1433;databaseName=FT546-shadow", "shadow_evidence"); SqlServerPermissionProbe probe = mock(SqlServerPermissionProbe.class); when(probe.verifySource(source)).thenReturn( PermissionCheckResult.rejected( "SOURCE_HAS_UPDATE_PERMISSION")); JdbcTemplate evidenceJdbc = mock(JdbcTemplate.class); SqlServerShadowDatabaseGate gate = new SqlServerShadowDatabaseGate( dataSource(source), dataSource(evidence), evidenceJdbc, probe); IllegalStateException failure = assertThrows( IllegalStateException.class, () -> gate.verify(properties())); assertEquals("SOURCE_HAS_UPDATE_PERMISSION", failure.getMessage()); verify(evidenceJdbc, never()).update(anyString(), any(), any(), any()); } @Test void acceptedDatabasesPersistAllSuppliedStartupChecksInEvidenceDatabase() throws Exception { Connection source = connection( "jdbc:sqlserver://prod:1433;databaseName=FT546-wcs", "shadow_ro"); Connection evidence = connection( "jdbc:sqlserver://evidence:1433;databaseName=FT546-shadow", "shadow_evidence"); SqlServerPermissionProbe probe = mock(SqlServerPermissionProbe.class); when(probe.verifySource(source)).thenReturn( PermissionCheckResult.accepted( "SOURCE_PERMISSIONS_VERIFIED")); when(probe.verifyEvidence(evidence)).thenReturn( PermissionCheckResult.accepted( "EVIDENCE_DATABASE_VERIFIED")); JdbcTemplate evidenceJdbc = mock(JdbcTemplate.class); SqlServerShadowDatabaseGate gate = new SqlServerShadowDatabaseGate( dataSource(source), dataSource(evidence), evidenceJdbc, probe); Map checks = gate.verify(properties()); checks.put("SHADOW_ENABLED", "PASS"); gate.persistChecks(checks); assertEquals("PASS", checks.get("DATABASE_IDENTITIES_DISTINCT")); assertEquals("PASS", checks.get("DATABASE_LOGINS_DISTINCT")); assertEquals("PASS", checks.get("SOURCE_PERMISSIONS_VERIFIED")); assertEquals("PASS", checks.get("EVIDENCE_DATABASE_VERIFIED")); assertEquals("PASS", checks.get("SHADOW_ENABLED")); verify(evidenceJdbc).batchUpdate(anyString(), any(java.util.List.class)); } private static DataSource dataSource(Connection connection) throws Exception { DataSource dataSource = mock(DataSource.class); when(dataSource.getConnection()).thenReturn(connection); return dataSource; } private static Connection connection(String url, String username) throws Exception { String database = url.substring( url.toLowerCase().indexOf("databasename=") + 13); return connection(url, username, url.substring(url.indexOf("//") + 2, url.indexOf(':', url.indexOf("//") + 2)), database); } private static Connection connection(String url, String username, String actualServer, String actualDatabase) throws Exception { return connection(url, username, actualServer, actualDatabase, 0); } private static Connection connection(String url, String username, String actualServer, String actualDatabase, int crossDatabaseAccess) throws Exception { Connection connection = mock(Connection.class); DatabaseMetaData metadata = mock(DatabaseMetaData.class); Statement statement = mock(Statement.class); ResultSet identity = mock(ResultSet.class); ResultSet access = mock(ResultSet.class); when(connection.getMetaData()).thenReturn(metadata); when(metadata.getURL()).thenReturn(url); when(metadata.getUserName()).thenReturn(username); when(connection.createStatement()).thenReturn(statement); when(statement.executeQuery(anyString())).thenAnswer(invocation -> invocation.getArgument(0).contains("HAS_DBACCESS") ? access : identity); when(identity.next()).thenReturn(true, false); when(identity.getString("server_name")).thenReturn(actualServer); when(identity.getString("database_name")) .thenReturn(actualDatabase); when(identity.getString("login_name")).thenReturn(username); when(access.next()).thenReturn(true, false); when(access.getObject("has_access")) .thenReturn(crossDatabaseAccess); return connection; } private static ShadowProperties properties() { ShadowProperties properties = new ShadowProperties(); properties.getSource().setJdbcUrl( "jdbc:sqlserver://prod:1433;databaseName=FT546-wcs"); properties.getSource().setUsername("shadow_ro"); properties.getEvidence().setJdbcUrl( "jdbc:sqlserver://evidence:1433;databaseName=FT546-shadow"); properties.getEvidence().setUsername("shadow_evidence"); return properties; } }