225 lines
10 KiB
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;
|
|
}
|
|
}
|