fix: scope admin infra pages to current tenant's tables and indices
All checks were successful
CI / build (push) Successful in 1m14s
CI / cleanup-branch (push) Has been skipped
CI / docker (push) Successful in 44s
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Successful in 35s

Database tables filtered to current_schema(), active queries to
current_database(), OpenSearch indices to configured index-prefix.
Delete endpoint rejects indices outside application scope.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-03-18 09:29:06 +01:00
parent f01487ccb4
commit 5a0a915cc6
2 changed files with 13 additions and 3 deletions

View File

@@ -72,13 +72,14 @@ public class DatabaseAdminController {
@Operation(summary = "Get table sizes and row counts") @Operation(summary = "Get table sizes and row counts")
public ResponseEntity<List<TableSizeResponse>> getTables() { public ResponseEntity<List<TableSizeResponse>> getTables() {
var tables = jdbc.query(""" var tables = jdbc.query("""
SELECT schemaname || '.' || relname AS table_name, SELECT relname AS table_name,
n_live_tup AS row_count, n_live_tup AS row_count,
pg_size_pretty(pg_total_relation_size(relid)) AS data_size, pg_size_pretty(pg_total_relation_size(relid)) AS data_size,
pg_total_relation_size(relid) AS data_size_bytes, pg_total_relation_size(relid) AS data_size_bytes,
pg_size_pretty(pg_indexes_size(relid)) AS index_size, pg_size_pretty(pg_indexes_size(relid)) AS index_size,
pg_indexes_size(relid) AS index_size_bytes pg_indexes_size(relid) AS index_size_bytes
FROM pg_stat_user_tables FROM pg_stat_user_tables
WHERE schemaname = current_schema()
ORDER BY pg_total_relation_size(relid) DESC ORDER BY pg_total_relation_size(relid) DESC
""", (rs, row) -> new TableSizeResponse( """, (rs, row) -> new TableSizeResponse(
rs.getString("table_name"), rs.getLong("row_count"), rs.getString("table_name"), rs.getLong("row_count"),
@@ -94,7 +95,7 @@ public class DatabaseAdminController {
SELECT pid, EXTRACT(EPOCH FROM (now() - query_start)) AS duration_seconds, SELECT pid, EXTRACT(EPOCH FROM (now() - query_start)) AS duration_seconds,
state, query state, query
FROM pg_stat_activity FROM pg_stat_activity
WHERE state != 'idle' AND pid != pg_backend_pid() WHERE state != 'idle' AND pid != pg_backend_pid() AND datname = current_database()
ORDER BY query_start ASC ORDER BY query_start ASC
""", (rs, row) -> new ActiveQueryResponse( """, (rs, row) -> new ActiveQueryResponse(
rs.getInt("pid"), rs.getDouble("duration_seconds"), rs.getInt("pid"), rs.getDouble("duration_seconds"),

View File

@@ -48,17 +48,20 @@ public class OpenSearchAdminController {
private final AuditService auditService; private final AuditService auditService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final String opensearchUrl; private final String opensearchUrl;
private final String indexPrefix;
public OpenSearchAdminController(OpenSearchClient client, RestClient restClient, public OpenSearchAdminController(OpenSearchClient client, RestClient restClient,
SearchIndexerStats indexerStats, AuditService auditService, SearchIndexerStats indexerStats, AuditService auditService,
ObjectMapper objectMapper, ObjectMapper objectMapper,
@Value("${opensearch.url:http://localhost:9200}") String opensearchUrl) { @Value("${opensearch.url:http://localhost:9200}") String opensearchUrl,
@Value("${opensearch.index-prefix:executions-}") String indexPrefix) {
this.client = client; this.client = client;
this.restClient = restClient; this.restClient = restClient;
this.indexerStats = indexerStats; this.indexerStats = indexerStats;
this.auditService = auditService; this.auditService = auditService;
this.objectMapper = objectMapper; this.objectMapper = objectMapper;
this.opensearchUrl = opensearchUrl; this.opensearchUrl = opensearchUrl;
this.indexPrefix = indexPrefix;
} }
@GetMapping("/status") @GetMapping("/status")
@@ -109,6 +112,9 @@ public class OpenSearchAdminController {
List<IndexInfoResponse> allIndices = new ArrayList<>(); List<IndexInfoResponse> allIndices = new ArrayList<>();
for (JsonNode idx : indices) { for (JsonNode idx : indices) {
String name = idx.path("index").asText(""); String name = idx.path("index").asText("");
if (!name.startsWith(indexPrefix)) {
continue;
}
if (!search.isEmpty() && !name.contains(search)) { if (!search.isEmpty() && !name.contains(search)) {
continue; continue;
} }
@@ -146,6 +152,9 @@ public class OpenSearchAdminController {
@Operation(summary = "Delete an OpenSearch index") @Operation(summary = "Delete an OpenSearch index")
public ResponseEntity<Void> deleteIndex(@PathVariable String name, HttpServletRequest request) { public ResponseEntity<Void> deleteIndex(@PathVariable String name, HttpServletRequest request) {
try { try {
if (!name.startsWith(indexPrefix)) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "Cannot delete index outside application scope");
}
boolean exists = client.indices().exists(r -> r.index(name)).value(); boolean exists = client.indices().exists(r -> r.index(name)).value();
if (!exists) { if (!exists) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Index not found: " + name); throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Index not found: " + name);