Files
railwaysystem/scripts/qa/QaDatabaseProbe.java
T

95 lines
8.5 KiB
Java

import java.sql.*;
import java.util.*;
import java.nio.file.*;
import java.nio.file.attribute.PosixFilePermissions;
import javax.sql.rowset.*;
/** Read-only identity/schema probe. Credentials only from process environment. */
public class QaDatabaseProbe {
static final String DATABASE = "railwaysystem_fusion_qa_20261013";
static void query(Connection c, String label, String sql) throws SQLException {
System.out.println("[" + label + "]");
try (Statement s = c.createStatement()) {
s.setQueryTimeout(15);
try (ResultSet r = s.executeQuery(sql)) {
int n = r.getMetaData().getColumnCount();
while (r.next()) {
List<String> row = new ArrayList<>();
for (int i = 1; i <= n; i++) row.add(String.valueOf(r.getObject(i)));
System.out.println(String.join(" | ", row));
}
}
}
}
public static void main(String[] args) throws Exception {
String url = System.getenv("QA_DB_URL");
// Validate target before even opening a connection; no URL credentials permitted.
if (url == null || !url.matches("jdbc:kingbase8://[A-Za-z0-9.:-]+/" + DATABASE + "(?:\\?.*)?")
|| url.matches("(?i).*[?&](user|password)=.*"))
throw new SecurityException("Refusing non-target URL or URL credentials");
if (!"fusion_qa_app".equals(System.getenv("QA_DB_USER")))
throw new SecurityException("Read-only probe requires fusion_qa_app");
Class.forName("com.kingbase8.Driver");
Properties p = new Properties(); p.setProperty("user", System.getenv("QA_DB_USER"));
p.setProperty("password", Objects.requireNonNull(System.getenv("QA_DB_PASSWORD")));
try (Connection c = DriverManager.getConnection(url, p)) {
c.setAutoCommit(false);
c.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
try (Statement s = c.createStatement()) { s.execute("SET TRANSACTION READ ONLY"); }
try (Statement s = c.createStatement(); ResultSet r = s.executeQuery("SELECT current_database()")) {
r.next(); if (!DATABASE.equals(r.getString(1))) throw new SecurityException("Database mismatch");
}
query(c,"identity", "SELECT current_database(), current_user, current_schema(), version()");
query(c,"server_endpoint", "SELECT inet_server_addr(),inet_server_port(),current_database(),current_user");
query(c,"read_only_transaction", "SHOW transaction_read_only");
query(c,"search_path", "SHOW search_path");
query(c,"schemas", "SELECT schema_name FROM information_schema.schemata ORDER BY schema_name");
// Kingbase information_schema.tables can require extra catalog privileges;
// reuse the readable pg_tables view rather than broadening the app role.
query(c,"table_inventory", "SELECT schemaname,count(*) FROM pg_tables WHERE schemaname IN ('public','iotlocks') GROUP BY schemaname ORDER BY schemaname");
query(c,"training_counts", "SELECT 'train_student',count(*) FROM public.train_student UNION ALL SELECT 'train_enrollment',count(*) FROM public.train_enrollment UNION ALL SELECT 'train_accommodation',count(*) FROM public.train_accommodation");
query(c,"round5s_prestate", "SELECT count(*) FROM information_schema.columns WHERE table_schema='iotlocks' AND table_name IN ('lock_auth','lock_command') AND column_name IN ('execution_mode','environment_id','intent_key','intent_version','execution_state','intent_action','intent_at','attempt_started_at')");
query(c,"columns", "SELECT table_schema,table_name,column_name,data_type,is_nullable,column_default "
+ "FROM information_schema.columns WHERE (table_schema='iotlocks' AND table_name IN ('lock_auth','lock_command')) ORDER BY table_name,ordinal_position");
query(c,"constraints", "SELECT table_schema,table_name,constraint_name,constraint_type FROM information_schema.table_constraints "
+ "WHERE table_schema IN ('iotlocks','public') AND table_name IN ('lock_auth','lock_command') ORDER BY table_name,constraint_name");
query(c,"counts", "SELECT 'lock_auth',count(*) FROM iotlocks.lock_auth UNION ALL SELECT 'lock_command',count(*) FROM iotlocks.lock_command");
query(c,"indexes", "SELECT schemaname,tablename,indexname,indexdef FROM pg_indexes WHERE schemaname='iotlocks' AND tablename IN ('lock_auth','lock_command') ORDER BY tablename,indexname");
query(c,"runtime_account", "SELECT rolname,rolsuper,rolcreatedb,rolcreaterole FROM pg_roles WHERE rolname=current_user");
query(c,"schema_privileges", "SELECT nspname,has_schema_privilege(current_user,oid,'USAGE'),has_schema_privilege(current_user,oid,'CREATE') FROM pg_namespace WHERE nspname IN ('public','iotlocks') ORDER BY nspname");
query(c,"table_owners", "SELECT schemaname,tablename,tableowner FROM pg_tables WHERE schemaname IN ('public','iotlocks') AND tablename IN ('sys_user','lock_auth','lock_command') ORDER BY schemaname,tablename");
query(c,"table_grants", "SELECT table_schema,privilege_type,count(*) FROM information_schema.role_table_grants WHERE grantee=current_user AND table_schema IN ('public','iotlocks') GROUP BY table_schema,privilege_type ORDER BY table_schema,privilege_type");
query(c,"extra_grant_review", "SELECT table_schema,table_name,privilege_type FROM information_schema.role_table_grants WHERE grantee=current_user AND table_schema IN ('public','iotlocks') AND privilege_type IN ('TRUNCATE','TRIGGER','REFERENCES') ORDER BY table_schema,table_name,privilege_type");
query(c,"target_sessions", "SELECT usename,application_name,state,count(*) FROM pg_stat_activity WHERE datname=current_database() GROUP BY usename,application_name,state ORDER BY usename,application_name,state");
if (args.length == 2 && "--backup".equals(args[0])) {
Path directory = Paths.get(args[1]);
Files.createDirectory(directory, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")));
for (String table : List.of("lock_auth", "lock_command")) {
Path destination = directory.resolve(table + ".xml");
Files.createFile(destination, PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rw-------")));
try (Statement s = c.createStatement(); ResultSet rows = s.executeQuery("SELECT * FROM iotlocks." + table + " ORDER BY 1");
WebRowSet backup = RowSetProvider.newFactory().createWebRowSet();
java.io.Writer writer = Files.newBufferedWriter(destination)) {
backup.populate(rows); backup.writeXml(writer);
System.out.println("PRIVATE_TABLE_SNAPSHOT " + table + " rows=" + backup.size());
}
}
System.out.println("TABLE_SNAPSHOT_ONLY: no native full-database dump or restore drill claimed");
}
if (Arrays.asList(args).contains("--require-migrated")) {
String common = "'execution_mode','environment_id','intent_key','intent_version','execution_state','intent_action','intent_at'";
try (Statement s = c.createStatement(); ResultSet r = s.executeQuery("SELECT count(*) FROM information_schema.columns WHERE table_schema='iotlocks' AND ((table_name='lock_auth' AND column_name IN (" + common + ")) OR (table_name='lock_command' AND column_name IN (" + common + ",'device_sn','attempt_started_at')))")) {
r.next(); if (r.getInt(1) != 16) throw new SecurityException("MIGRATION_REQUIRED: runtime refused");
}
try (Statement s = c.createStatement(); ResultSet r = s.executeQuery("SELECT count(*) FROM pg_indexes WHERE schemaname='iotlocks' AND indexname IN ('uq_round5s_command_intent','uq_round5s_auth_intent','ix_round5s_command_recovery','ix_round5s_auth_recovery')")) {
r.next(); if (r.getInt(1) != 4) throw new SecurityException("MIGRATION_INDEXES_REQUIRED: runtime refused");
}
}
c.rollback(); System.out.println("READ_ONLY_ROLLBACK_COMPLETE");
} catch (SQLException e) {
// Server exceptions can contain values; expose SQLSTATE only.
System.err.println("QA_DATABASE_PROBE_FAILED SQLSTATE=" + e.getSQLState()); System.exit(2);
}
}
}