Skip to content

Commit 4366ef0

Browse files
notSumit25claudevenkateshsakamuri-lab
authored
fix: authorize every connection-scoped endpoint (116 were unguarded) (#86)
12 controllers took a caller-supplied connectionId and never checked it. SecurityConfig only asserts .anyRequest().authenticated() and no filter, interceptor or aspect inspects a connection id, so authentication was the only barrier. Verified against a running install, not inferred: a DEVELOPER holding no grant on any connection could - read literal-bearing slow-query SQL with real customer ids and names (GET /slow-query-analytics/{id}/query/{fp}/samples returned 200 while GET /slow-log-source/{id} returned 403 in the same session), - enumerate another tenant's schema and table statistics, via two endpoints that decrypt the target connection's credentials and open a live JDBC session (/tenant-column-suggestions and /config), - and permanently delete that tenant's analysis history (DELETE /slow-queries/history/connection/{id} -> 200, row gone). Affected: SlowQueryController (43), SlowQueryAnalyticsController (13), SchemaChangeController (13), SentinelAnalyticsController (10), PerformanceActionController (9), QueryPerformanceController (8), QueryPlanController (8), IndexAdvisorController (7), PerformanceInsightsController (5), AdvisorController (3), ResourceLimitsController (3), BusinessRuleController (3). This is the same class of defect BrainController carried (93 of 116 unguarded). The safety test added then hardcodes one Path.of(...), so it could not see any of these. What changed * 127 guard calls: assertCanReadConnectionContent on reads, assertCanManageConnectionContent on writes and deletes. * An id is not a capability. For endpoints keyed on alertId, actionId, regressionId, recommendationId, fingerprintId, planId, ruleId, snapshotId or historyId, resolve the owning connection and assert on that. 15 new findConnectionIdFor* accessors where no lookup existed. These report 404, not 403, for an unknown id — a 403 confirms the row exists, turning the endpoint into an id oracle, and regressionId is a sequential Long. * Ids arriving in the request body are not constrained by a path-variable check. Four holes survived exactly that kind of fix: - schema-changes/snapshots/compare took two snapshot ids and no connectionId at all, so it would diff tenant A's schema against tenant B's; compareSnapshots now refuses a mismatch outright. - PUT /performance-actions/batch-status took an arbitrary actionIds list with no scope; it now authorizes every id before mutating any, so a mixed batch fails atomically. - changes/acknowledge and regressions/acknowledge authorized the path connection and then acted on whatever ids the body named; allChangesBelongTo / allComparisonsBelongTo verify membership, and an id that resolves to nothing fails too, so unknown ids cannot be mixed into an otherwise valid batch. * Never take the actor from the request. userId was a query parameter and acknowledgedBy/resolvedBy/updatedBy defaulted to the literal string "user", so the acknowledgement trail was unauthenticated free text that could name any colleague. 10 sites now use requireCurrentUsername(). The parameters are still accepted for wire compatibility and ignored. * ConnectionScopedAuthorizationSafetyTest replaces the per-file approach: it scans every *Controller.java, so a new controller is covered the day it is written. Six cases — connection-scoped endpoints authorized, body-supplied id collections scoped, 403 not swallowed into 500, controller advices not swallowing denials, exemptions still true, delegated service checks still present. The exemption list is itself guarded, so it cannot rot into a way of hiding a real gap. Two things found by writing and running the fix, not by reading it * The generalized test immediately found 9 more unguarded endpoints in controllers nobody was looking at: StatsController, ProjectController, DashboardController, and a destructive DELETE /sentinel/demo/cleanup/{connectionId}. Three had been in my draft exemption list on the assumption they were connection-free; they were not. * Testing the fix found a bug reading it never would. 24 endpoints returned 403 and index-advisor returned 500: IndexAdvisorExceptionHandler's @ExceptionHandler(Exception.class) swallowed the denial and reported "Index operation failed" with the 403's text in the body. The guard held, but the response blamed the index store. It now handles ResponseStatusException first, and the safety test asserts no advice with a catch-all omits that. Also drops @crossorigin(origins = "*") from SentinelAnalyticsController. Tested and inert — an evil-origin preflight gets 403 with no Access-Control-Allow-Origin because the SecurityConfig allowlist wins, while an allowed origin gets 200 + ACAO — but it reads like an intentional hole. Verification Real Maven compile of main and test sources, zero errors. SlowQueryControllerS3Test needed the new constructor argument and was updated rather than left red. Live, against the rebuilt image with a DEVELOPER holding no grant on the target connection: - 40/40 previously-leaking reads -> 403 - 10/10 writes and destructive endpoints -> 403, and psql confirms nothing was mutated - 7/7 orphan-id and body-scoped paths -> 404, no existence oracle - 30/30 same endpoint shapes on a granted connection -> 200, zero false denials; confirmed again from a real browser session - index-advisor now returns 403 "Read access denied for this connection" Not covered: mvn test was not executed (the image build uses -DskipTests and this host has no JDK/Maven). The six safety-test cases were validated by re-implementing their scan logic against the tree and the file compiles, but they have not been run by JUnit. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
1 parent b04683a commit 4366ef0

28 files changed

Lines changed: 1277 additions & 35 deletions

CLAUDE.md

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,103 @@ it against a real database — not a theoretical hardening pass.
709709
`catch (ResponseStatusException e) { throw e; }` a 403 is swallowed and reported as a
710710
server error, so a client cannot tell "not yours" from "broken". The safety test
711711
asserts this too.
712+
- **Then it happened again, on 12 more controllers — 116 endpoints, zero checks.**
713+
`BrainControllerAuthorizationSafetyTest` hardcodes one `Path.of(...)`, so it could not
714+
see `SlowQueryController` (43), `SlowQueryAnalyticsController` (13),
715+
`SchemaChangeController` (13), `SentinelAnalyticsController` (10),
716+
`PerformanceActionController` (9), `QueryPerformanceController` (8),
717+
`QueryPlanController` (8), `IndexAdvisorController` (7),
718+
`PerformanceInsightsController` (5), `AdvisorController` (3),
719+
`ResourceLimitsController` (3) or `BusinessRuleController` (3). Verified live, not
720+
inferred: a DEVELOPER holding **no grant on any connection** read literal-bearing
721+
slow-query SQL with real customer ids and names
722+
(`/slow-query-analytics/{id}/query/{fp}/samples` → 200 while
723+
`/slow-log-source/{id}` → 403 in the same session), enumerated another tenant's
724+
schema, and **deleted that tenant's analysis history** via
725+
`DELETE /slow-queries/history/connection/{id}`. All 116 are now guarded.
726+
`ConnectionScopedAuthorizationSafetyTest` replaces the per-file approach: it scans
727+
**every** `*Controller.java`, so a new controller is covered the day it is written.
728+
Writing it immediately found 9 more unguarded endpoints in controllers nobody was
729+
looking at, including `StatsController`, `ProjectController`, `DashboardController`
730+
and a destructive `DELETE /sentinel/demo/cleanup/{connectionId}`.
731+
- **Two endpoints decrypted another user's credentials before anyone checked access.**
732+
`GET /slow-query-analytics/{id}/tenant-column-suggestions` and `/config` reach
733+
`suggestTenantColumns``getJdbcTemplateForBackgroundJob`
734+
`credentialService.getDecryptedConnection`, opening a live JDBC session to the target
735+
database. An unguarded read is not only a data leak; it can be a credential-use
736+
primitive. Check before the work, not after.
737+
- **A path-variable sweep is not enough — ids in the request body need their own
738+
check.** Four holes survived exactly that kind of fix: `snapshots/compare` (two
739+
snapshot ids, no `connectionId` at all — it would diff tenant A's schema against
740+
tenant B's), `PUT /performance-actions/batch-status` (an arbitrary `actionIds` list,
741+
no scope), and `changes/acknowledge` / `regressions/acknowledge` (path connection
742+
authorized, body ids unchecked). `allChangesBelongTo` / `allComparisonsBelongTo`
743+
verify membership, and **an id that resolves to nothing fails too** — otherwise
744+
unknown ids can be mixed into an otherwise valid batch. The safety test has a
745+
dedicated case for body-supplied id collections.
746+
- **An id is not a capability.** For `alertId`, `actionId`, `regressionId`,
747+
`recommendationId`, `fingerprintId`, `planId`, `ruleId`, `snapshotId`, `historyId`:
748+
resolve the owning connection and assert on that. Several services had no such
749+
accessor, so `findConnectionIdFor*` was added to `QueryPerformanceService`,
750+
`QueryPlanCacheService`, `SentinelAnalyticsService`, `BusinessRuleMemoryService`,
751+
`SlowQueryAlertService`, `QueryFingerprintService` and `SchemaChangeTrackingService`.
752+
These helpers report **404 for both** "no such id" and "not yours", via
753+
`assertCanRead/ManageConnectionContentOrNotFound`. The first attempt only 404'd the
754+
*unknown* case and left an authorized-but-denied row at 403, which still confirms the
755+
row exists — a review caught that the code comments claimed a property the code did not
756+
have. `query_performance_regression.id` is a sequential `Long`, so walking 1..N would
757+
have mapped every tenant's regressions. Same answer
758+
`DashboardWorkspaceService.assertCanReadDashboard` already gives. Endpoints keyed on a
759+
**connectionId** keep 403: the caller already knows that connection exists, so an
760+
actionable "access denied" is better than a misleading 404.
761+
- **Never take the actor from the request.** `POST /slow-queries/alerts/{id}/acknowledge`
762+
took `@RequestParam String userId`; `acknowledgedBy` defaulted to the literal string
763+
`"user"`; `resolvedBy`, `updatedBy` and Sentinel's `initiatedBy` came from the request
764+
body — so the audit trail was unauthenticated free text and could name any colleague.
765+
All of them now use `accessControlService.requireCurrentUsername()`. The parameters are
766+
still accepted (wire compatibility) and ignored, which is noted at each site so nobody
767+
re-wires them.
768+
- **Guarded vs unguarded is an existence oracle.** A guarded endpoint 404s an unknown
769+
connection id (`resolveCurrentUserAccess` wraps the lookup); an unguarded one returned
770+
200. That difference alone enumerated valid connection ids.
771+
- **A scanner built on an allowlist of id names can only catch the ids someone
772+
remembered.** `ConnectionScopedAuthorizationSafetyTest` first matched
773+
`body.contains("connectionId")` plus a hand-written list
774+
(`alertId|actionId|regressionId|…`). Both halves leaked: `ProjectController.createProject`
775+
reads `request.getConnectionId()`**capital C** — and `projectId` was not in the list,
776+
so `POST /projects` and `GET|PUT|DELETE /projects/{projectId}` were invisible while the
777+
suite reported every case green. Now the connection match is case-insensitive and *any*
778+
`@PathVariable …Id` counts as connection-owned until proven otherwise, with genuine
779+
exceptions in `NOT_CONNECTION_OWNED_IDS` carrying a reason. Inverting it immediately
780+
surfaced four `PlaybookController` endpoints — those turned out to be true negatives
781+
(`Playbook` has no `connectionId`; playbooks are global templates), and
782+
`playbookExemptionHoldsOnlyWhilePlaybooksAreConnectionFree` fails the build if a
783+
`connectionId` is ever added to that entity. **A safety test that reports green is
784+
evidence only about what it can see.**
785+
- **Two path variables are as dangerous as a body id.**
786+
`POST /schema-changes/{connectionId}/snapshots/{snapshotId}/set-baseline` authorized the
787+
connection and then flipped *whatever snapshot id it was handed* to BASELINE and pointed
788+
that connection's drift config at it — so manage access on A could retarget B's snapshot
789+
and bind A's baseline to it. `setBaseline` now refuses a snapshot whose `connectionId`
790+
differs, in the service as well as the controller, and **throws rather than silently
791+
skipping**: no-op'ing the snapshot write while still writing the drift config would leave
792+
the config pointing at another connection's snapshot. When a handler takes an id
793+
alongside a `connectionId`, authorizing the connection is half the check.
794+
- **A `@ControllerAdvice` catch-all swallows a 403 the same way an in-method one does,
795+
and it is easier to miss because it lives in another file.**
796+
`IndexAdvisorExceptionHandler` has `@ExceptionHandler(Exception.class)`, so the newly
797+
added guard on `/index-advisor/{id}/health-report` returned
798+
`500 "Index operation failed"` with the 403's text in the body — the denial held, but
799+
the response blamed the index store. Found by *testing the fix*, not by reading it: the
800+
other 24 endpoints returned 403 and this one did not. It now has an
801+
`@ExceptionHandler(ResponseStatusException.class)` that preserves the status, and
802+
`ConnectionScopedAuthorizationSafetyTest` asserts every advice with a catch-all also
803+
handles `ResponseStatusException`.
804+
- **`@CrossOrigin(origins = "*")` on a controller is dead code here, and worth
805+
deleting.** `SentinelAnalyticsController` carried it. Tested: an evil-origin preflight
806+
gets `403` with no `Access-Control-Allow-Origin` (the `SecurityConfig` allowlist wins),
807+
while an allowed origin gets `200` + ACAO — so the annotation never had effect. It
808+
still reads like an intentional hole to the next person.
712809

713810
### MCP & CLI Release Rules
714811

backend/src/main/java/com/dbaagent/controller/AdvisorController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,31 @@
33
import com.dbaagent.model.IndexRecommendation;
44
import com.dbaagent.model.PerformanceAnalysis;
55
import com.dbaagent.service.DatabaseAdvisorService;
6+
import com.dbaagent.service.security.AccessControlService;
67
import lombok.RequiredArgsConstructor;
78
import lombok.extern.slf4j.Slf4j;
89
import org.springframework.http.ResponseEntity;
910
import org.springframework.web.bind.annotation.*;
1011

1112
import java.util.List;
1213

14+
/**
15+
* REST API for the performance advisor (analysis, missing indexes, health summary).
16+
*
17+
* <p><b>Authorization:</b> every endpoint here takes a caller-supplied connection id, so
18+
* each one asserts access itself ({@code assertCanReadConnectionContent} for reads,
19+
* {@code assertCanManageConnectionContent} for writes). {@code SecurityConfig} only
20+
* requires an authenticated principal — nothing upstream inspects a connection id. See
21+
* {@code ConnectionScopedAuthorizationSafetyTest}.
22+
*/
1323
@RestController
1424
@RequestMapping("/advisor")
1525
@RequiredArgsConstructor
1626
@Slf4j
1727
public class AdvisorController {
1828

1929
private final DatabaseAdvisorService advisorService;
30+
private final AccessControlService accessControlService;
2031

2132
/**
2233
* Get comprehensive performance analysis
@@ -25,6 +36,7 @@ public class AdvisorController {
2536
public ResponseEntity<PerformanceAnalysis> analyzePerformance(
2637
@PathVariable String connectionId
2738
) {
39+
accessControlService.assertCanReadConnectionContent(connectionId);
2840
try {
2941
log.info("Performance analysis requested for connection: {}", connectionId);
3042
PerformanceAnalysis analysis = advisorService.analyzePerformance(connectionId);
@@ -44,6 +56,7 @@ public ResponseEntity<PerformanceAnalysis> analyzePerformance(
4456
public ResponseEntity<List<IndexRecommendation>> getMissingIndexes(
4557
@PathVariable String connectionId
4658
) {
59+
accessControlService.assertCanReadConnectionContent(connectionId);
4760
try {
4861
log.info("Index recommendations requested for connection: {}", connectionId);
4962
PerformanceAnalysis analysis = advisorService.analyzePerformance(connectionId);
@@ -63,6 +76,7 @@ public ResponseEntity<List<IndexRecommendation>> getMissingIndexes(
6376
public ResponseEntity<HealthSummary> getHealthSummary(
6477
@PathVariable String connectionId
6578
) {
79+
accessControlService.assertCanReadConnectionContent(connectionId);
6680
try {
6781
log.info("Health summary requested for connection: {}", connectionId);
6882
PerformanceAnalysis analysis = advisorService.analyzePerformance(connectionId);

backend/src/main/java/com/dbaagent/controller/BusinessRuleController.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.dbaagent.model.brain.BrainRule;
44
import com.dbaagent.service.BusinessRuleMemoryService;
5+
import com.dbaagent.service.security.AccessControlService;
56
import lombok.RequiredArgsConstructor;
67
import org.springframework.http.ResponseEntity;
78
import org.springframework.web.bind.annotation.*;
@@ -11,13 +12,20 @@
1112

1213
/**
1314
* API endpoints for connection-scoped learned SQL business rules.
15+
*
16+
* <p><b>Authorization:</b> every endpoint here takes a caller-supplied connection id, so
17+
* each one asserts access itself ({@code assertCanReadConnectionContent} for reads,
18+
* {@code assertCanManageConnectionContent} for writes). {@code SecurityConfig} only
19+
* requires an authenticated principal — nothing upstream inspects a connection id. See
20+
* {@code ConnectionScopedAuthorizationSafetyTest}.
1421
*/
1522
@RestController
1623
@RequestMapping("/business-rules")
1724
@RequiredArgsConstructor
1825
public class BusinessRuleController {
1926

2027
private final BusinessRuleMemoryService businessRuleMemoryService;
28+
private final AccessControlService accessControlService;
2129

2230
/**
2331
* Returns all active rules for the connection plus the subset applicable to an optional question.
@@ -26,6 +34,7 @@ public class BusinessRuleController {
2634
public ResponseEntity<Map<String, Object>> getRules(
2735
@PathVariable String connectionId,
2836
@RequestParam(required = false) String question) {
37+
accessControlService.assertCanReadConnectionContent(connectionId);
2938
List<BrainRule> activeRules = businessRuleMemoryService.getActiveRules(connectionId);
3039
List<BusinessRuleMemoryService.SqlGuardrail> applicable = businessRuleMemoryService
3140
.resolveApplicableGuardrails(connectionId, question, null);
@@ -49,6 +58,7 @@ public ResponseEntity<Map<String, Object>> getRules(
4958
public ResponseEntity<Map<String, Object>> learn(
5059
@PathVariable String connectionId,
5160
@RequestBody LearnRuleRequest request) {
61+
accessControlService.assertCanManageConnectionContent(connectionId);
5262
int learned = businessRuleMemoryService.learnFromFeedback(
5363
connectionId,
5464
request.text(),
@@ -70,6 +80,10 @@ public ResponseEntity<Map<String, Object>> learn(
7080
*/
7181
@DeleteMapping("/rule/{ruleId}")
7282
public ResponseEntity<Map<String, Object>> deactivateRule(@PathVariable String ruleId) {
83+
String connectionId = businessRuleMemoryService.findConnectionIdForRule(ruleId)
84+
.orElseThrow(() -> new org.springframework.web.server.ResponseStatusException(
85+
org.springframework.http.HttpStatus.NOT_FOUND, "Rule not found"));
86+
accessControlService.assertCanManageConnectionContent(connectionId);
7387
boolean deactivated = businessRuleMemoryService.deactivateRule(ruleId);
7488
return ResponseEntity.ok(Map.of(
7589
"ruleId", ruleId,

backend/src/main/java/com/dbaagent/controller/DashboardController.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
package com.dbaagent.controller;
22

33
import com.dbaagent.service.DashboardService;
4+
import com.dbaagent.service.security.AccessControlService;
45
import lombok.RequiredArgsConstructor;
56
import lombok.extern.slf4j.Slf4j;
67
import org.springframework.http.ResponseEntity;
78
import org.springframework.web.bind.annotation.*;
89

910
/**
1011
* REST API for performance dashboard
12+
*
13+
* <p><b>Authorization:</b> every endpoint here takes a caller-supplied connection id, so
14+
* each one asserts access itself ({@code assertCanReadConnectionContent} for reads,
15+
* {@code assertCanManageConnectionContent} for writes). {@code SecurityConfig} only
16+
* requires an authenticated principal — nothing upstream inspects a connection id. See
17+
* {@code ConnectionScopedAuthorizationSafetyTest}.
1118
*/
1219
@RestController
1320
@RequestMapping("/dashboard")
@@ -16,6 +23,7 @@
1623
public class DashboardController {
1724

1825
private final DashboardService dashboardService;
26+
private final AccessControlService accessControlService;
1927

2028
/**
2129
* Get performance dashboard data for a connection
@@ -26,6 +34,7 @@ public ResponseEntity<DashboardService.DashboardData> getPerformanceDashboard(
2634
@RequestParam(required = false, defaultValue = "30") Integer days
2735
) {
2836
try {
37+
accessControlService.assertCanReadConnectionContent(connectionId);
2938
log.info("Fetching performance dashboard for connection: {}, days: {}", connectionId, days);
3039

3140
DashboardService.DashboardData data = dashboardService.getDashboardData(connectionId, days);

backend/src/main/java/com/dbaagent/controller/IndexAdvisorController.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.dbaagent.service.IndexAdvisorService;
44
import com.dbaagent.service.PerformanceMonitoringService;
5+
import com.dbaagent.service.security.AccessControlService;
56
import lombok.RequiredArgsConstructor;
67
import lombok.extern.slf4j.Slf4j;
78
import org.springframework.http.ResponseEntity;
@@ -12,6 +13,12 @@
1213

1314
/**
1415
* REST API for enhanced index advisor functionality
16+
*
17+
* <p><b>Authorization:</b> every endpoint here takes a caller-supplied connection id, so
18+
* each one asserts access itself ({@code assertCanReadConnectionContent} for reads,
19+
* {@code assertCanManageConnectionContent} for writes). {@code SecurityConfig} only
20+
* requires an authenticated principal — nothing upstream inspects a connection id. See
21+
* {@code ConnectionScopedAuthorizationSafetyTest}.
1522
*/
1623
@RestController
1724
@RequestMapping("/index-advisor")
@@ -21,12 +28,14 @@ public class IndexAdvisorController {
2128

2229
private final IndexAdvisorService indexAdvisorService;
2330
private final PerformanceMonitoringService performanceMonitoringService;
31+
private final AccessControlService accessControlService;
2432

2533
/**
2634
* Get comprehensive index health report
2735
*/
2836
@GetMapping("/{connectionId}/health-report")
2937
public ResponseEntity<Map<String, Object>> getHealthReport(@PathVariable String connectionId) {
38+
accessControlService.assertCanReadConnectionContent(connectionId);
3039
return ResponseEntity.ok(indexAdvisorService.getIndexHealthReport(connectionId));
3140
}
3241

@@ -35,6 +44,7 @@ public ResponseEntity<Map<String, Object>> getHealthReport(@PathVariable String
3544
*/
3645
@GetMapping("/{connectionId}/unused")
3746
public ResponseEntity<List<Map<String, Object>>> getUnusedIndexes(@PathVariable String connectionId) {
47+
accessControlService.assertCanReadConnectionContent(connectionId);
3848
return ResponseEntity.ok(performanceMonitoringService.getUnusedIndexes(connectionId));
3949
}
4050

@@ -43,6 +53,7 @@ public ResponseEntity<List<Map<String, Object>>> getUnusedIndexes(@PathVariable
4353
*/
4454
@GetMapping("/{connectionId}/duplicates")
4555
public ResponseEntity<List<Map<String, Object>>> getDuplicateIndexes(@PathVariable String connectionId) {
56+
accessControlService.assertCanReadConnectionContent(connectionId);
4657
return ResponseEntity.ok(performanceMonitoringService.getDuplicateIndexes(connectionId));
4758
}
4859

@@ -53,6 +64,7 @@ public ResponseEntity<List<Map<String, Object>>> getDuplicateIndexes(@PathVariab
5364
public ResponseEntity<Map<String, Object>> estimateIndexCreation(
5465
@PathVariable String connectionId,
5566
@RequestBody Map<String, Object> request) {
67+
accessControlService.assertCanReadConnectionContent(connectionId);
5668

5769
String tableName = (String) request.get("tableName");
5870
@SuppressWarnings("unchecked")
@@ -75,6 +87,7 @@ public ResponseEntity<Map<String, Object>> estimateIndexCreation(
7587
public ResponseEntity<Map<String, Object>> estimateIndexDrop(
7688
@PathVariable String connectionId,
7789
@RequestBody Map<String, Object> request) {
90+
accessControlService.assertCanReadConnectionContent(connectionId);
7891

7992
String tableName = (String) request.get("tableName");
8093
String indexName = (String) request.get("indexName");
@@ -94,6 +107,7 @@ public ResponseEntity<Map<String, Object>> estimateIndexDrop(
94107
public ResponseEntity<List<Map<String, Object>>> getIndexUsageStats(
95108
@PathVariable String connectionId,
96109
@PathVariable String tableName) {
110+
accessControlService.assertCanReadConnectionContent(connectionId);
97111
return ResponseEntity.ok(performanceMonitoringService.getIndexUsageStats(connectionId, tableName));
98112
}
99113

@@ -102,6 +116,7 @@ public ResponseEntity<List<Map<String, Object>>> getIndexUsageStats(
102116
*/
103117
@GetMapping("/{connectionId}/cache-stats")
104118
public ResponseEntity<Map<String, Double>> getCacheStats(@PathVariable String connectionId) {
119+
accessControlService.assertCanReadConnectionContent(connectionId);
105120
return ResponseEntity.ok(performanceMonitoringService.getCacheHitRatios(connectionId));
106121
}
107122
}

0 commit comments

Comments
 (0)