You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 9577e9d
Browse filesBrowse the repository at this point in the historyBrowse files
Adds a pin toggle in Manage Connections and in the sidebar connection
switcher. A pinned connection is the one DeepSQL opens on every load.
The pin is per user, not a flag on the connection. A connection can be
shared through connection_access_grant, so a column on database_connection
would let one person's choice decide what everyone else opens on — and
shared connections are canManageConfig=false for their recipients, so
exactly the people who most want a default could not set one. One pin per
user, enforced by a unique constraint on connection_pin.username; pinning a
second connection moves the row rather than adding one.
PUT|DELETE /connections/{id}/pin are gated on assertCanUseConnection, not
assertCanManageConnectionConfig: choosing where you land is a preference,
not a change to the connection. GET /connections carries the pinned flag per
caller, so no surface needs a second request, and deleteConnection clears
every pin on the connection alongside its grants.
The pin has to beat an already-selected connection, not just an empty one.
useDashboardStore persists connectionId, so after a reload something is
always selected and the existing auto-select never ran. useConnectionManager
now applies the pin once per page load, tracked at module scope rather than
in a ref — the hook is called from a dozen sections, and a per-instance
guard would let a later-mounted section yank the user back to the pin after
they deliberately switched.
The sidebar toggle is there because Manage Connections needs
MANAGE_CONNECTIONS to open at all; without it a Developer or Data Engineer,
who typically holds exactly one granted connection, would have no way to set
a default.
ConnectionScopedAuthorizationSafetyTest now flags GET /connections, because
the handler resolves the caller's pinned connection id and the scanner
matches (?i)connection_?id anywhere in a handler body. That endpoint takes
no arguments at all, so it is exempted in AUTHORIZED_ELSEWHERE and
connectionListingTakesNoCallerSuppliedId re-derives the claim, so the
exemption cannot rot into cover for a real gap. The scanner was not
weakened and no meaningless assert was added.
Verified against a live backend on a throwaway stack: ddl-auto creates
connection_pin with its unique index; pinning a second connection flips the
first; unpinning a non-pinned connection leaves the real pin alone; deleting
a connection clears its pin; a DEVELOPER with no grant gets 403 (not 500);
a DEVELOPER holding only a grant pins successfully and their pin does not
appear on the admin's list.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: CLAUDE.md
+45Lines changed: 45 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -498,6 +498,51 @@ so "View as" resolves membership as the target user).
498
498
"authentication is not authorization" trap `BrainController` documents — there is still
499
499
no filter doing it for you.
500
500
501
+
### Default connection (pinning)
502
+
503
+
A user can pin one connection as their default, from the pin column in **Manage
504
+
Connections** or the pin toggle in the sidebar connection switcher.
505
+
506
+
-**The pin is per user, not per connection.**`connection_pin` keys on username with a
507
+
unique constraint (`V120__create_connection_pin.sql`; applied by `ddl-auto` from the
508
+
`ConnectionPin` entity — verified on a scratch database, the table and its unique index
509
+
are created on boot). A column on `database_connection` would have been wrong twice
510
+
over: a connection shared through `connection_access_grant` would let one user's choice
511
+
decide what everyone else opens on, and a shared connection is `canManageConfig=false`
512
+
for its recipients, so exactly the people who most want a default could not set one.
513
+
-**One pin per user is the point.**`ConnectionPinService.pin` moves the existing row
514
+
rather than inserting a second; the unique constraint is the backstop, and a losing
515
+
concurrent insert re-reads and updates instead of surfacing a 500.
516
+
-**`PUT|DELETE /connections/{id}/pin` are gated on `assertCanUseConnection`**, not
517
+
`assertCanManageConnectionConfig` — choosing where you land is a preference, not a
518
+
change to the connection. Verified live: a DEVELOPER holding only a grant on a
519
+
connection (`canManageConfig: false`) pins it and gets 200, while the same user pinning
520
+
a connection they hold no grant on gets **403, not 500** — the `ResponseStatusException`
521
+
rethrow before the catch-all is doing its job.
522
+
-**Unpin is scoped to the connection named.** A stale click in a background tab must not
523
+
clear a pin the user has since moved elsewhere.
524
+
-**`GET /connections` carries `pinned` per caller**, so no surface needs a second
525
+
request, and two users listing the same shared connection see different values —
526
+
confirmed live. `deleteConnection` clears every pin on the connection alongside its
527
+
grants.
528
+
-**`ConnectionScopedAuthorizationSafetyTest` flags `GET /connections` now**, because the
529
+
handler resolves the caller's *pinned* connection id and the scanner matches
530
+
`(?i)connection_?id` anywhere in a handler body. That endpoint takes no arguments at all
531
+
— it returns whatever `getConnectionsForUser(username, isAdmin)` gives — so it is in
532
+
`AUTHORIZED_ELSEWHERE`, and `connectionListingTakesNoCallerSuppliedId` re-derives that
533
+
claim so the exemption cannot rot into cover for a real gap. Do not resolve this by
534
+
adding a meaningless assert, and do not weaken the scanner.
535
+
-**The pin must beat an already-selected connection, not just an empty one.**
536
+
`useDashboardStore` persists `connectionId`, so after a reload something is always
537
+
selected — the original auto-select ran only when nothing was. `useConnectionManager`
538
+
therefore applies the pin once per page load (`pinAppliedThisLoad`, module scope, reset
539
+
by `resetConnectionPinApplied()` in the auth reset). Module scope and not a ref: the
540
+
hook is called from a dozen sections, and a per-instance guard would let a
541
+
later-mounted section yank the user back to the pin after they deliberately switched.
542
+
Switching mid-session still sticks; the pin re-applies on the next load.
543
+
- Pinned connections sort first in `useConnectionManager`, so every consumer — the sidebar
544
+
switcher included — shows the default at the top.
545
+
501
546
### Admin profile switch
502
547
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.
0 commit comments