95 lines
8.5 KiB
Java
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);
|
|
}
|
|
}
|
|
}
|