Files
FT546-WCS/ruoyi-wcs-shadow/src/test/java/com/ruoyi/shadow/startup/SqlServerShadowDatabaseGateTest.java

225 lines
10 KiB
Java

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<String, String> 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.<String>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;
}
}