Skip to content

Commit 2d2b57c

Browse files
Merge origin/main into feat/preflight-migration-review
Resolved conflicts in ConnectionScopedAuthorizationSafetyTest.java: - Kept both MigrationRiskController exemption (from #100) and ConnectionController.getAllConnections exemption (from main) - Both exemptions are valid for different reasons and do not conflict Co-authored-by: Venkat SF <venkatesh.sakamuri@stayflexi.com>
2 parents 68f2452 + 62dc056 commit 2d2b57c

83 files changed

Lines changed: 10711 additions & 207 deletions

File tree

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: 110 additions & 1 deletion
Large diffs are not rendered by default.

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

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.dbaagent.repository.ConnectionInitHistoryRepository;
1010
import com.dbaagent.repository.ConnectionInitStatusRepository;
1111
import com.dbaagent.repository.SchemaDocumentationRepository;
12+
import com.dbaagent.service.ConnectionPinService;
1213
import com.dbaagent.service.ConnectionService;
1314
import com.dbaagent.service.scheduler.BrainInitSchedulerService;
1415
import com.dbaagent.service.scheduler.BrainJobsService;
@@ -45,6 +46,7 @@ public class ConnectionController {
4546
private final AccessControlService accessControlService;
4647
private final ConnectionAccessService connectionAccessService;
4748
private final com.dbaagent.repository.ConnectionAccessGrantRepository connectionAccessGrantRepository;
49+
private final ConnectionPinService connectionPinService;
4850

4951
@PostMapping("/test")
5052
public ResponseEntity<Map<String, Object>> testConnection(@RequestBody ConnectionRequest request) {
@@ -429,8 +431,10 @@ public ResponseEntity<List<ConnectionSummaryResponse>> getAllConnections() {
429431
String username = accessControlService.getCurrentUsername();
430432
boolean isAdmin = accessControlService.isCurrentUserAdmin();
431433
List<DatabaseConnection> connections = credentialService.getConnectionsForUser(username, isAdmin);
434+
// One lookup for the whole list rather than one per row.
435+
String pinnedId = connectionPinService.pinnedConnectionId(username).orElse(null);
432436
List<ConnectionSummaryResponse> decryptedConnections = connections.stream()
433-
.map(conn -> toSummary(conn, username, isAdmin))
437+
.map(conn -> toSummary(conn, username, isAdmin, pinnedId))
434438
.toList();
435439
return ResponseEntity.ok(decryptedConnections);
436440
} catch (org.springframework.web.server.ResponseStatusException e) {
@@ -440,13 +444,62 @@ public ResponseEntity<List<ConnectionSummaryResponse>> getAllConnections() {
440444
}
441445
}
442446

447+
/**
448+
* Pin this connection as the caller's default, replacing any connection they had
449+
* pinned before.
450+
*
451+
* <p>Gated on {@code assertCanUseConnection} rather than
452+
* {@code assertCanManageConnectionConfig}: choosing which database you land on is a
453+
* personal preference, not a change to the connection, and a shared connection is
454+
* config-read-only for its recipients. Requiring manage rights would mean the people
455+
* who most want a default — the ones who were granted exactly one connection —
456+
* could not set one.
457+
*/
458+
@PutMapping("/{id}/pin")
459+
public ResponseEntity<Map<String, Object>> pinConnection(@PathVariable String id) {
460+
Map<String, Object> response = new HashMap<>();
461+
try {
462+
accessControlService.assertCanUseConnection(id);
463+
connectionPinService.pin(accessControlService.requireCurrentUsername(), id);
464+
response.put("success", true);
465+
response.put("pinned", true);
466+
return ResponseEntity.ok(response);
467+
} catch (org.springframework.web.server.ResponseStatusException e) {
468+
throw e;
469+
} catch (Exception e) {
470+
response.put("success", false);
471+
response.put("message", "Failed to pin connection: " + e.getMessage());
472+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
473+
}
474+
}
475+
476+
/** Clear the caller's default, if this connection is the one currently pinned. */
477+
@DeleteMapping("/{id}/pin")
478+
public ResponseEntity<Map<String, Object>> unpinConnection(@PathVariable String id) {
479+
Map<String, Object> response = new HashMap<>();
480+
try {
481+
accessControlService.assertCanUseConnection(id);
482+
connectionPinService.unpin(accessControlService.requireCurrentUsername(), id);
483+
response.put("success", true);
484+
response.put("pinned", false);
485+
return ResponseEntity.ok(response);
486+
} catch (org.springframework.web.server.ResponseStatusException e) {
487+
throw e;
488+
} catch (Exception e) {
489+
response.put("success", false);
490+
response.put("message", "Failed to unpin connection: " + e.getMessage());
491+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
492+
}
493+
}
494+
443495
@DeleteMapping("/{id}")
444496
public ResponseEntity<Map<String, Object>> deleteConnection(@PathVariable String id) {
445497
Map<String, Object> response = new HashMap<>();
446498
try {
447499
accessControlService.assertCanManageConnectionConfig(id);
448500
connectionService.closeConnectionPool(id);
449501
connectionAccessService.deleteAllGrantsForConnection(id);
502+
connectionPinService.clearPinsForConnection(id);
450503
credentialService.deleteConnection(id);
451504
response.put("success", true);
452505
response.put("message", "Connection deleted successfully");
@@ -781,7 +834,7 @@ public ResponseEntity<?> runBrainJob(@PathVariable String id, @PathVariable Stri
781834
}
782835
}
783836

784-
private ConnectionSummaryResponse toSummary(DatabaseConnection conn, String username, boolean isAdmin) {
837+
private ConnectionSummaryResponse toSummary(DatabaseConnection conn, String username, boolean isAdmin, String pinnedConnectionId) {
785838
ConnectionSummaryResponse summary = new ConnectionSummaryResponse();
786839
try {
787840
ConnectionRequest decrypted = credentialService.getDecryptedConnection(conn.getId());
@@ -821,6 +874,7 @@ private ConnectionSummaryResponse toSummary(DatabaseConnection conn, String user
821874
summary.setAccessLevel(resolved.getEffectiveAccess().name());
822875
summary.setCanManageConfig(resolved.canManageConfig());
823876
summary.setCanManageContent(resolved.canManageContent());
877+
summary.setPinned(conn.getId() != null && conn.getId().equals(pinnedConnectionId));
824878
return summary;
825879
}
826880

0 commit comments

Comments
 (0)