Skip to content

Commit f90f60f

Browse files
fix: View as Agent must enforce the target user's data policy (#71)
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## What was going wrong A **new Agent chat thread** while Viewing as `marts-editor` still returned `crm.customers` rows (including `amount`). SQL Editor already blocked `crm`. Direct MCP API calls with a marts-editor token also blocked `crm`. The Agent path did not. End-to-end logs for the failing turn (`session=fca723ca8171`, 14:14 UTC): 1. `/api/agent/session` minted MCP token **257** for `marts-editor` and provisioned `u-marts-editor`. 2. Hermes `POST /api/profile/switch` returned **200** three times; the session JSON is tagged `"profile": "u-marts-editor"`. 3. `mcp__deepsql__get_brain_context` and `mcp__deepsql__execute_sql` authenticated as **admin** token **253**. 4. Audit: `EDITOR_QUERY_EXECUTED` / `clientType=mcp` / `user_id=1` (`admin@demo.local`) / `SELECT … FROM crm.customers` / `rowCount=2`. 5. Token 257 `last_used_at` is only the Spring `probeMcpAuth` right after mint — Hermes never sent it. 6. `u-marts-editor` has **no** `mcp-stderr.log`; `u-admin` MCP was started on Aug 18 and reused. Hermes keeps **one** DeepSQL MCP stdio process, started from the first loaded profile (`u-admin`). `POST /api/profile/switch` is explicitly `process_wide=False` (cookie / thread-local only). A new chat does not respawn MCP. `resolveEffectivePolicy(..., actorIsAdmin=true)` returns `none()`, so schema and column policy never run. `probeMcpAuth` only proves the minted token works against Spring, not that the live MCP process will use it. ## Fix `scripts/local-agent-provisioner.py` (the Agent container's `/provision` handler) now mirrors the minted token onto `$HERMES_HOME/deepsql.token` **and every** `profiles/*/deepsql.token`. The long-lived MCP client re-reads `DEEPSQL_TOKEN_FILE` per request (mtime cache), so the next `execute_sql` authenticates as the viewed-as user and policy applies. Chat-path policy is also fail-closed for unparseable/unhandled SQL and missing actors, and walks CTEs, UNIONs, and nested `FROM`/`WHERE` selects. ## Merge with main Resolved conflicts with `main` (`#70` whole-statement schema allowlist + nested protected-table inspectability, `#72` brain endpoint authz). Kept this branch's recursive SELECT walker and fail-closed parse/actor rules. Took main's `assertProtectedTablesAreInspectable` / `namesMatch` so a qualified protection (`public.customer_profiles`) does not catch the same table name in another schema, while an unqualified nested reference still fails closed. `UserDataAccessPolicyServiceTest` (24) and `BrainControllerAuthorizationSafetyTest` (2) pass after the merge. ## How to verify 1. View as `marts-editor`, Agent tab, **New chat**. 2. Ask: `show me all customer information from crm tables`. 3. Expect a policy block, not Acme/Globex rows. `security_event.user_id` for that SQL must be the editor, not admin. Rebuild/restart `deepsql-agent` so the container picks up the provisioner change, or copy `scripts/local-agent-provisioner.py` onto `/opt/deepsql-agent/provisioner.py` and restart the provisioner process, then reopen the Agent tab (triggers `/provision`). ## Re-test after the fix Same prompt in a new Agent thread while Viewing as marts-editor. Agent refused CRM; audit `user_id=2` / `clientAgent=marts-editor`. [Agent refuses CRM while viewing as marts-editor](https://cursor.com/agents/bc-019fe687-99b1-76fd-80fa-dd213aecc497/artifacts?path=%2Fopt%2Fcursor%2Fartifacts%2Fagent_view_as_crm_blocked.webp) [agent_view_as_crm_policy_block.mp4](https://cursor.com/agents/bc-019fe687-99b1-76fd-80fa-dd213aecc497/artifacts?path=%2Fopt%2Fcursor%2Fartifacts%2Fagent_view_as_crm_policy_block.mp4) <sub>To show artifacts inline, <a href="https://cursor.com/dashboard/cloud-agents#my-pull-requests">enable</a> in settings.</sub> <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-019fe687-99b1-76fd-80fa-dd213aecc497?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a href="https://cursor.com/background-agent?bcId=bc-019fe687-99b1-76fd-80fa-dd213aecc497&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
1 parent 95b81f9 commit f90f60f

43 files changed

Lines changed: 1664 additions & 227 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -199,8 +199,9 @@ returns a number).
199199
2. **LLM Provider Registry**: Use `LlmProviderRegistry` for all provider-specific LLM behavior. Do NOT add if/else or switch on provider type. Chat and embedding providers are registered and resolved independently — some providers offer only one. Providers are *factories* over credentials, not `ChatModel`s, so credentials stay resolvable per call and key rotation needs no restart.
200200
3. **SSH-Aware Access**: Always use `ConnectionService.getJdbcTemplate(connectionId, request)` — handles SSH tunneling transparently.
201201
4. **SQL Rule**: All generated SQL MUST use table-qualified column names (`table.column_name`).
202-
5. **RAG Caching**: Three-tier cache (memory → Redis → Azure Search). Redis failure is graceful (app continues without caching).
203-
6. **Virtual Threads**: Enabled for concurrency (JDK 25).
202+
5. **Chat access policy**: Fail closed. Walk the whole SQL tree (CTEs, set ops, subqueries). Deny unparseable or unhandled statements. Require an actor except `INTERNAL`/`SCHEDULED`. MCP/Editor identity comes from `SecurityContext`, not `QueryActorContextHolder`. Persist `allowed_schemas`. Do not let "how many" override a protected-column mention. Public share is refused when the connection has an active policy.
203+
6. **RAG Caching**: Three-tier cache (memory → Redis → Azure Search). Redis failure is graceful (app continues without caching).
204+
7. **Virtual Threads**: Enabled for concurrency (JDK 25).
204205

205206
### Frontend Rules
206207
1. **API Centralization**: ALL API calls through `src/lib/api/client.js`. Never create direct axios instances.
@@ -210,7 +211,13 @@ returns a number).
210211
5. **Design**: Minimal black/white/grey palette, Inter font, subtle transitions. See UX guidelines in full CLAUDE.md.
211212

212213
### Admin profile switch
213-
Admins can **View as** a sub-user from the top-right of the home layout (`ProfileSwitch`) to verify connection ACLs, chat/editor policies, and role-gated nav. The admin JWT stays on the session; `ImpersonationService` sets an httpOnly `impersonate_user` cookie and `JwtAuthenticationFilter` overlays the target principal. `POST|DELETE|GET /api/admin/impersonate` are excluded from the overlay so stop/list still run as the real admin. Cannot target another ADMIN, self, or a non-ACTIVE account. `/auth/me` returns the **effective** user plus `impersonating` / `impersonatorUsername`.
214+
Admins can **View as** a sub-user from the top-right of the home layout (`ProfileSwitch`) to verify connection ACLs, chat/editor policies, and role-gated nav.
215+
216+
The admin JWT **subject** stays the administrator so logout, refresh, and `/admin/impersonate` still own the real session. Policy identity is the target: an httpOnly `impersonate_user` cookie plus an `impUid` claim on the access token. `JwtAuthenticationFilter` overlays that principal onto the SecurityContext for every request except the impersonation control plane, logout, and session refresh. Chat, Editor, schema listing, and Agent MCP calls then run `AccessControlService` / `ConnectionChatAccessPolicyService` as the target (`actorIsAdmin` is false, so policies apply).
217+
218+
The Agent tab must not inherit the admin MCP token. `/api/agent/session` mints an MCP token for the effective user and never falls back to the admin session JWT while View as is active. nginx `auth_request` on `/agent-api` forwards `/api/auth/me`'s `X-Remote-User` (the overlaid username) instead of hardcoding `admin`.
219+
220+
`POST|DELETE|GET /api/admin/impersonate` are excluded from the overlay so stop/list still run as the real admin. Cannot target another ADMIN, self, or a non-ACTIVE account. `/auth/me` returns the **effective** user plus `impersonating` / `impersonatorUsername`.
214221

215222
### Git Rules
216223
- Do NOT commit automatically — wait for explicit user instruction.
@@ -238,7 +245,16 @@ Admins can **View as** a sub-user from the top-right of the home layout (`Profil
238245
`hermes_requires <0.20.0` on a "verified" 401 that came from a hand-rolled
239246
`hermes serve` run rather than `hermes webui`. 0.20.0 works. Verify against the
240247
real start path before writing a version constraint.
241-
5. **The agent image build clones two third-party repos over the public internet,
248+
5. **Hermes MCP is process-global.** One `deepsql-phase1-server.js` stdio server
249+
is started from the first loaded profile (usually `u-admin`) and lives until
250+
the Agent API process dies. `POST /api/profile/switch` is `process_wide=False`
251+
on purpose (per-browser cookie), so a View as / new-thread Agent chat tagged
252+
`profile: u-marts-editor` still sends the admin MCP bearer. Chat-access policy
253+
then `resolveEffectivePolicy(..., actorIsAdmin=true)``none()`. The
254+
provisioner mirrors the target user's token onto every `deepsql.token` the
255+
live process might re-read (`DEEPSQL_TOKEN_FILE` mtime cache in
256+
`mcp/deepsql-phase1-lib.js`).
257+
6. **The agent image build clones two third-party repos over the public internet,
242258
unauthenticated.** `agent/Dockerfile` fetches `NousResearch/hermes-agent` and
243259
`nesquena/hermes-webui` at build time. GitHub rate-limits unauthenticated
244260
requests *per source IP*, and Actions runners share pooled egress addresses, so
@@ -276,6 +292,16 @@ broken. Assert the *outcome*, never the attempt:
276292
- **`set -e` + `read` at EOF aborts silently.** Prompts in `install.sh` use
277293
`read … || true` so the explicit emptiness checks report the problem. Without it the
278294
installer exited 1 with no message, after writing generated secrets to `.env`.
295+
- **Minting an Agent MCP token ≠ Hermes using it.** `/api/agent/session` can mint
296+
`u-marts-editor`'s token and `POST /api/profile/switch` can 200 while
297+
`execute_sql` still authenticates as admin. Hermes keeps one DeepSQL MCP
298+
stdio process (started from the first profile that loaded `mcp_servers`) and
299+
`profile/switch` is `process_wide=False`. A new chat thread does not respawn
300+
MCP. `probeMcpAuth` only proves the *minted* token works against Spring, not
301+
that the live MCP process will send it. The provisioner must mirror the
302+
token onto every `deepsql.token` the live process might be watching
303+
(`scripts/local-agent-provisioner.py`). Audit: `security_event.user_id` on
304+
`EDITOR_QUERY_EXECUTED` with `clientType=mcp`.
279305
- **Silent-failure rule, concretely:** the CLI rendered an unreachable server as
280306
`No databases connected yet` because one `catch` covered both the connection fetch
281307
and decorative extras. An unreachable host must never look like an empty account.

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,21 @@ public ResponseEntity<?> refreshSession(HttpServletRequest httpRequest, HttpServ
207207
User effectiveUser = impersonationService.resolveFromCookie(httpRequest, user)
208208
.map(ImpersonationContext.State::target)
209209
.orElse(user);
210-
// Keep the user's agent token alive for as long as the UI session lives.
211-
// The SPA refreshes on access-token expiry (~every 15 min of activity), so
212-
// this slides the agent token forward on each active interval — a logged-in
213-
// UI never ends up with a dead agent.
210+
if (effectiveUser != user && effectiveUser.getId() != null) {
211+
authSessionService.reissueAccessToken(
212+
httpResponse,
213+
session.getId(),
214+
user,
215+
effectiveUser.getId()
216+
);
217+
}
218+
// Keep agent tokens alive for as long as the UI session lives.
219+
// During View as the SPA still refreshes the *admin* session; also
220+
// slide the target user's minted MCP token or their Agent tab dies.
214221
agentBridgeService.extendAgentTokens(user.getUsername());
222+
if (!effectiveUser.getUsername().equals(user.getUsername())) {
223+
agentBridgeService.extendAgentTokens(effectiveUser.getUsername());
224+
}
215225
Map<String, Object> payload = toAuthPayload(
216226
effectiveUser,
217227
effectiveUser.getRoleEnum(),

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import com.dbaagent.service.brain.query.PlanPatternLibraryService;
6666
import com.dbaagent.service.brain.BrainInsightEmbeddingService;
6767
import com.dbaagent.service.SlowQueryHistoryService;
68+
import com.dbaagent.service.UserDataAccessPolicyService;
6869
import com.dbaagent.service.security.AccessControlService;
6970
import com.dbaagent.model.SlowQueryAnalysis;
7071
import lombok.RequiredArgsConstructor;
@@ -139,6 +140,7 @@ public class BrainController {
139140
private final SlowQueryHistoryService slowQueryHistoryService;
140141
private final BrainInsightEmbeddingService brainInsightEmbeddingService;
141142
private final AccessControlService accessControlService;
143+
private final UserDataAccessPolicyService userDataAccessPolicyService;
142144

143145
@GetMapping("/understanding/{connectionId}")
144146
public ResponseEntity<BrainUnderstandingResponse> getUnderstanding(
@@ -531,7 +533,12 @@ public ResponseEntity<List<InferredTableRelationship>> getInferredRelationships(
531533
) {
532534
try {
533535
accessControlService.assertCanReadConnectionContent(connectionId);
534-
return ResponseEntity.ok(joinRelationshipInferenceService.getRelationships(connectionId));
536+
return ResponseEntity.ok(userDataAccessPolicyService.filterInferredRelationships(
537+
connectionId,
538+
accessControlService.getCurrentUsername(),
539+
accessControlService.isCurrentUserAdmin(),
540+
joinRelationshipInferenceService.getRelationships(connectionId)
541+
));
535542
} catch (ResponseStatusException e) {
536543
throw e;
537544
} catch (Exception e) {

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,10 @@ public ResponseEntity<?> query(@RequestBody DashboardQueryRequest request) {
7777
qr.setExecutionOrigin(QueryExecutionOrigin.API);
7878
QueryResult result = queryExecutorService.executeQuery(
7979
request.connectionId(), qr,
80-
QueryExecutionContext.api(accessControlService.getCurrentUsername()));
80+
QueryExecutionContext.api(
81+
accessControlService.getCurrentUsername(),
82+
accessControlService.isCurrentUserAdmin()
83+
));
8184
return ResponseEntity.ok(Map.of(
8285
"success", true,
8386
"columns", result.getColumns() == null ? java.util.List.of() : result.getColumns(),

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import com.dbaagent.model.QueryResult;
99
import com.dbaagent.service.ExplainPlanService;
1010
import com.dbaagent.service.McpSqlGuardService;
11-
import com.dbaagent.service.QueryActorContextHolder;
1211
import com.dbaagent.service.QueryExecutionContext;
1312
import com.dbaagent.service.QueryExecutionPolicyException;
1413
import com.dbaagent.service.QueryExecutorService;
@@ -77,7 +76,10 @@ public ResponseEntity<?> executeReadOnlyQuery(@RequestBody McpReadOnlyQueryReque
7776
QueryResult result = queryExecutorService.executeQuery(
7877
request.getConnectionId(),
7978
queryRequest,
80-
QueryExecutionContext.mcp(QueryActorContextHolder.currentUsername())
79+
QueryExecutionContext.mcp(
80+
accessControlService.getCurrentUsername(),
81+
accessControlService.isCurrentUserAdmin()
82+
)
8183
);
8284
return ResponseEntity.ok(Map.of(
8385
"success", true,

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

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

33
import com.dbaagent.model.DashboardVersion;
44
import com.dbaagent.model.SavedDashboard;
5+
import com.dbaagent.service.ConnectionChatAccessPolicyService;
56
import com.dbaagent.service.SavedDashboardService;
67
import com.dbaagent.service.security.AccessControlService;
78
import lombok.extern.slf4j.Slf4j;
@@ -27,6 +28,9 @@ public class SavedDashboardController {
2728
@Autowired
2829
private AccessControlService accessControlService;
2930

31+
@Autowired
32+
private ConnectionChatAccessPolicyService connectionChatAccessPolicyService;
33+
3034
// Every write method below is load-then-save on a row a background generation
3135
// turn (SavedDashboardService.beginGenerationTurn etc.) may be writing at the
3236
// same time. Without this helper, the loser's raw Hibernate message
@@ -47,6 +51,13 @@ public ResponseEntity<Map<String, Object>> enableShare(@PathVariable UUID id) {
4751
SavedDashboard existing = savedDashboardService.getDashboardById(id)
4852
.orElseThrow(() -> new IllegalArgumentException("Dashboard not found"));
4953
accessControlService.assertCanReadConnectionContent(existing.getConnectionId());
54+
if (connectionChatAccessPolicyService.hasActivePolicy(existing.getConnectionId())) {
55+
return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of(
56+
"success", false,
57+
"errorCode", "POLICY_PUBLIC_SHARE_FORBIDDEN",
58+
"message", "This connection has an active chat access policy, so the dashboard cannot be shared publicly."
59+
));
60+
}
5061
SavedDashboard d = savedDashboardService.enablePublicShare(id);
5162
return ResponseEntity.ok(Map.of("success", true,
5263
"shareToken", d.getShareToken(), "isPublic", true));

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import com.dbaagent.service.QueryExecutionContext;
88
import com.dbaagent.service.QueryExecutionPolicyException;
99
import com.dbaagent.service.ActiveQueryService;
10+
import com.dbaagent.service.McpTokenService;
1011
import com.dbaagent.service.QueryExecutorService;
1112
import com.dbaagent.service.RunningQueryRegistry;
1213
import com.dbaagent.service.SqlExecutionAuditService;
@@ -15,6 +16,7 @@
1516
import com.dbaagent.service.SchemaScannerService;
1617
import com.dbaagent.service.VisualizationService;
1718
import com.dbaagent.service.security.AccessControlService;
19+
import org.springframework.http.HttpHeaders;
1820
import jakarta.servlet.http.HttpServletRequest;
1921
import lombok.RequiredArgsConstructor;
2022
import lombok.extern.slf4j.Slf4j;
@@ -195,11 +197,7 @@ public ResponseEntity<Map<String, Object>> executeQuery(
195197
QueryResult result = queryExecutorService.executeQuery(
196198
connectionId,
197199
queryRequest,
198-
QueryExecutionContext.editor(
199-
accessControlService.getCurrentUsername(),
200-
accessControlService.isCurrentUserAdmin(),
201-
Boolean.TRUE.equals(queryRequest.getMutationConfirmed())
202-
)
200+
queryExecutionContext(queryRequest, httpRequest)
203201
);
204202
sqlExecutionAuditService.record(SqlExecutionAuditService.AuditRecord.executed()
205203
.connectionId(connectionId)
@@ -413,6 +411,26 @@ public ResponseEntity<Map<String, Object>> getTableStats(
413411
}
414412
}
415413

414+
private QueryExecutionContext queryExecutionContext(QueryRequest queryRequest, HttpServletRequest httpRequest) {
415+
String username = accessControlService.getCurrentUsername();
416+
boolean admin = accessControlService.isCurrentUserAdmin();
417+
if (isMcpBearer(httpRequest)) {
418+
return QueryExecutionContext.mcp(username, admin);
419+
}
420+
return QueryExecutionContext.editor(
421+
username,
422+
admin,
423+
Boolean.TRUE.equals(queryRequest.getMutationConfirmed())
424+
);
425+
}
426+
427+
private boolean isMcpBearer(HttpServletRequest httpRequest) {
428+
String authorization = httpRequest.getHeader(HttpHeaders.AUTHORIZATION);
429+
return authorization != null
430+
&& authorization.startsWith("Bearer ")
431+
&& authorization.substring(7).startsWith(McpTokenService.TOKEN_PREFIX);
432+
}
433+
416434
private SchemaMetadata scopedSchema(String connectionId, SchemaMetadata schema) {
417435
return userDataAccessPolicyService.filterSchemaMetadata(
418436
connectionId,

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

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.dbaagent.service.SemanticModelService;
1111
import com.dbaagent.service.TrainingJobService;
1212
import com.dbaagent.service.TrainingService;
13+
import com.dbaagent.service.UserDataAccessPolicyService;
1314
import com.dbaagent.service.security.AccessControlService;
1415
import lombok.Data;
1516
import lombok.RequiredArgsConstructor;
@@ -37,6 +38,7 @@ public class TrainingController {
3738
private final CredentialRepository credentialRepository;
3839
private final SemanticModelService semanticModelService;
3940
private final AccessControlService accessControlService;
41+
private final UserDataAccessPolicyService userDataAccessPolicyService;
4042

4143
/**
4244
* Train with schema DDL
@@ -186,7 +188,9 @@ public ResponseEntity<Map<String, Object>> debugRetrieve(
186188
if (question == null || question.isBlank()) {
187189
return ResponseEntity.badRequest().body(Map.of("error", "Query parameter 'q' is required"));
188190
}
189-
return ResponseEntity.ok(trainingService.debugRetrieve(connectionId, question, topK));
191+
Map<String, Object> payload = trainingService.debugRetrieve(connectionId, question, topK);
192+
filterDebugRetrieval(connectionId, payload);
193+
return ResponseEntity.ok(payload);
190194
} catch (org.springframework.web.server.ResponseStatusException e) {
191195
throw e;
192196
} catch (Exception e) {
@@ -346,6 +350,34 @@ private void rebuildAndReindexConnection(String connectionId) {
346350
trainingService.reindexConnection(connectionId);
347351
}
348352

353+
@SuppressWarnings("unchecked")
354+
private void filterDebugRetrieval(String connectionId, Map<String, Object> payload) {
355+
if (payload == null) {
356+
return;
357+
}
358+
Object results = payload.get("results");
359+
if (!(results instanceof List<?> rows)) {
360+
return;
361+
}
362+
List<Map<String, Object>> filtered = new java.util.ArrayList<>();
363+
for (Object row : rows) {
364+
if (!(row instanceof Map<?, ?> map)) {
365+
continue;
366+
}
367+
Object metadata = map.get("metadata");
368+
if (userDataAccessPolicyService.isRagMetadataInScope(
369+
connectionId,
370+
accessControlService.getCurrentUsername(),
371+
accessControlService.isCurrentUserAdmin(),
372+
metadata == null ? null : String.valueOf(metadata)
373+
)) {
374+
filtered.add((Map<String, Object>) map);
375+
}
376+
}
377+
payload.put("results", filtered);
378+
payload.put("resultCount", filtered.size());
379+
}
380+
349381
@Data
350382
public static class DocumentationRequest {
351383
private String connectionId;

backend/src/main/java/com/dbaagent/dto/ConnectionChatAccessPolicyResponse.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ public class ConnectionChatAccessPolicyResponse {
1616
List<String> blockedSensitivityCategories;
1717
List<String> deniedTables;
1818
List<String> deniedColumns;
19+
List<String> allowedSchemas;
20+
boolean allowAggregates;
1921
boolean blockMode;
2022
boolean redactMode;
2123
boolean active;

backend/src/main/java/com/dbaagent/dto/PolicyPreviewResponse.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public class PolicyPreviewResponse {
1111
List<String> blockedSensitivityCategories;
1212
List<String> deniedTables;
1313
List<String> deniedColumns;
14+
List<String> allowedSchemas;
15+
boolean allowAggregates;
1416
List<String> impactedTables;
1517
List<String> impactedColumns;
1618
boolean blockMode;

0 commit comments

Comments
 (0)