feat: add chat sharing API#24968
Conversation
Docs preview📖 View docs preview for |
100cc49 to
a001ff0
Compare
a001ff0 to
baeaf16
Compare
be8d940 to
1f00c89
Compare
baeaf16 to
806e74d
Compare
1f00c89 to
57c2c31
Compare
806e74d to
f77ce50
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Solid ACL implementation that correctly mirrors the workspace sharing model with proper transaction handling, defense-in-depth authorization, and thorough test coverage. The sub-chat TOCTOU protection (outer check + inner FOR UPDATE re-check) is well done. The generic UpdateValidator extension to support ChatRole is clean. Test coverage is comprehensive: lifecycle, sub-chat inheritance, validation, kill switch, stream access, and list exclusion.
Two P2 concerns, six P3 concerns, one P4, and five nits.
The headline issue is the group member PII leak: GET /acl returns full ReducedUser records (emails, login types, statuses) for every group member to any shared reader, while the comment claims only "member counts" are needed. Eight reviewers independently converged on this. The workspace ACL handler acknowledges the same pattern as security debt with a TODO; the chat handler copies the pattern without the acknowledgment. This is the single highest-value fix in this review.
The pubsub contract split is the second concern: three call sites still use db2sdk.Chat() which leaves OwnerUsername as "" in serialized events, while the SDK type declares it as a required string. Currently benign (frontend merge is correct, events go to owner only), but structurally wrong for any future consumer. Adding omitempty to OwnerUsername would at least align the serialization contract with OwnerName.
"The whole point is fresh eyes from a reviewer whose triggers did NOT match" was validated by Razor, who caught the dead sentinel panic path that the triggered reviewers all walked past.
coderd/exp_chats.go:123
P2 [DEREM-4] publishChatTitleChange emits Chat with empty OwnerUsername/OwnerName because it uses db2sdk.Chat() instead of ChatWithOwner.
Three pubsub producers were not updated to the new hydrated converter: publishChatTitleChange (here), publishChatPubsubEvent (chatd.go:4889), and publishChatActionRequired (chatd.go:4936). The Chat.OwnerUsername field has no omitempty tag, so the JSON always includes "owner_username": "". Meanwhile OwnerName has omitempty and is cleanly absent. This asymmetry means a consumer cannot distinguish "not provided" from "empty" for OwnerUsername but can for OwnerName.
Currently benign: the frontend spreads ...cachedChat as base so empty fields from events don't overwrite cached values. But any new consumer that trusts the event as a complete snapshot will silently lose owner identity.
Fix either:
- Add
omitemptytoOwnerUsernameso both owner fields have consistent absent-when-empty semantics, or - Hydrate owner info in the pubsub paths (the
database.Chatalready carriesOwnerID).
(Hisoka P2, Mafuuu P2, Meruem P3, Ryosuke P3, Zoro P3, Chopper P3)
🤖
coderd/rbac/regosql/configs.go:53
P3 [DEREM-13] The ChatConverter inherits resourceIDMatcher() which generates unqualified id :: text. With the new JOIN users in GetChats, both tables expose an id column. If a future RBAC policy emits an id-based filter clause, PostgreSQL rejects the query with "column reference 'id' is ambiguous." Current policies don't emit id clauses, so this is latent. Fix: override with sqltypes.StringVarMatcher("chats.id :: text", []string{"input", "object", "id"}) in ChatConverter. (Knuckle P3, Hisoka Note)
🤖
🤖 This review was automatically generated with Coder Agents.
57c2c31 to
32efc17
Compare
7f45110 to
fbb5847
Compare
32efc17 to
d43fb9f
Compare
fbb5847 to
52f8308
Compare
295dbe5 to
d00ecb3
Compare
52f8308 to
8f9483a
Compare
d00ecb3 to
ac20edb
Compare
8f9483a to
bdfd540
Compare
f01b1d5 to
dfc4533
Compare
bdfd540 to
ae29c15
Compare
C013 (DEREM-4): Partially fixed in |
C014 (DEREM-13): Fixed in |
|
/coder-agents-review |
7e8ecf0 to
c544179
Compare
e65ee30 to
4161734
Compare
|
/coder-agents-review |
There was a problem hiding this comment.
Post-human-review verification complete. The R6 structural changes (removal of canonicalChatACLKeys, removal of writeChatACLSubChatError abstraction, comment cleanups) introduce no regressions.
Mafuuu re-raised the canonicalization root cause: without normalization, non-canonical UUID keys from non-standard API clients create phantom ACL entries that appear with wrong roles and cannot be deleted. The human reviewer explicitly chose this direction ("We have control over the client. See what we do with workspace ACL."), making chat ACL consistent with workspace ACL. The consequence is documented in the inventory as a human-decided trade-off (DEREM-24, Note).
The simplified handlers are cleaner: sub-chat checks write 400 directly instead of routing through a panic-guarded helper. Authorization boundaries remain correct across the simplification.
🤖 This review was automatically generated with Coder Agents.
| if chat.RootChatID.Valid { | ||
| resp.Detail = "Target the root chat (id: " + chat.RootChatID.UUID.String() + ") instead." | ||
| } |
There was a problem hiding this comment.
For other readers: chat.RootChatID could be zero if it's a third-level-deep subchat.
There was a problem hiding this comment.
We should probably track the owning chat even on Nth depth?
mafredri
left a comment
There was a problem hiding this comment.
Other than system restricted, LGTM 👍🏻
| if chat.RootChatID.Valid { | ||
| resp.Detail = "Target the root chat (id: " + chat.RootChatID.UUID.String() + ") instead." | ||
| } |
There was a problem hiding this comment.
We should probably track the owning chat even on Nth depth?
| // may not otherwise be readable by the caller. | ||
| var err error | ||
| //nolint:gocritic | ||
| dbGroups, err = api.Database.GetGroups(dbauthz.AsSystemRestricted(ctx), database.GetGroupsParams{GroupIds: groupIDs}) |
There was a problem hiding this comment.
Why do we need as system restricted here? The gocritic nolint also has no explanation. We should try to avoid it if we can (e.g. as chatd).
There was a problem hiding this comment.
Gotcha. Let's at least attach reasoning to the nolint line: //nolint:gocritic // [Reason why we're disabling it.]. It should state why, not just link to context (the linked PR suffers from the same issue). Adding the reason to the nolint line ensures it never gets disconnected.
Note
🤖 This PR was written by Coder Agent on behalf of Danielle Maywood
Builds on the merged chat sharing foundation (#25041).
Adds the public experimental chat sharing API on top of the foundation PR. This includes chat ACL read/update handlers, SDK request/response types and client methods, ACL lifecycle coverage, and generated API/TypeScript updates.
Chat responses also expose owner username/name so the frontend can identify the chat owner without requiring separate user read access.
Implementation plan