fix: improve chat audit log descriptions and diff rendering (#25728)

Chat ACL audit diffs rendered as `[object Object]` because the diff
viewer called `.toString()` on object values. Common chat operations
(archive, share) showed generic "updated chat" descriptions instead of
semantic ones.

Add `chatAuditLogDescription` to derive semantic descriptions from the
audit diff for successful chat writes: "archived/unarchived chat" for
archive toggles, "updated sharing for chat" for ACL-only changes.
Extract diff value formatting into `formatAuditDiffValue`, which renders
object values as deterministic compact JSON with sorted keys, fixing the
`[object Object]` rendering for chat ACLs and any other object-valued
fields. The previous `determineIdPSyncMappingDiff` workaround for IdP
sync mappings was removed because the generic formatting handles it.

Closes CODAGT-513

> Generated by Coder Agents on behalf of @johnstcn
This commit is contained in:
Cian Johnston
2026-05-28 18:37:57 +01:00
committed by GitHub
parent ebf56ebd12
commit 7ea0eff94e
7 changed files with 381 additions and 73 deletions
+56
View File
@@ -303,6 +303,12 @@ func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
_, _ = b.WriteString("{user} ")
}
// Chat write operations get semantic descriptions derived from the diff.
if desc, ok := chatAuditLogDescription(alog); ok {
_, _ = b.WriteString(desc)
return b.String()
}
switch {
case alog.AuditLog.StatusCode == int32(http.StatusSeeOther):
_, _ = b.WriteString("was redirected attempting to ")
@@ -345,6 +351,56 @@ func auditLogDescription(alog database.GetAuditLogsOffsetRow) string {
return b.String()
}
// chatAuditLogDescription returns a description for successful chat write
// operations based on the diff contents. It returns false for non-chat
// resources, non-write actions, or error/redirect status codes, letting
// the caller fall through to the generic description.
func chatAuditLogDescription(alog database.GetAuditLogsOffsetRow) (string, bool) {
if alog.AuditLog.ResourceType != database.ResourceTypeChat ||
alog.AuditLog.Action != database.AuditActionWrite ||
alog.AuditLog.StatusCode >= 400 ||
alog.AuditLog.StatusCode == int32(http.StatusSeeOther) {
return "", false
}
var diff codersdk.AuditDiff
if err := json.Unmarshal(alog.AuditLog.Diff, &diff); err != nil {
return "", false
}
// Single "archived" field: archive or unarchive.
if len(diff) == 1 {
if field, ok := diff["archived"]; ok {
oldVal, oldOK := field.Old.(bool)
newVal, newOK := field.New.(bool)
if oldOK && newOK {
if !oldVal && newVal {
return "archived chat {target}", true
}
if oldVal && !newVal {
return "unarchived chat {target}", true
}
}
}
}
// All fields are ACL changes: sharing update.
if len(diff) > 0 {
aclOnly := true
for field := range diff {
if field != "user_acl" && field != "group_acl" {
aclOnly = false
break
}
}
if aclOnly {
return "updated sharing for chat {target}", true
}
}
return "", false
}
func (api *API) auditLogIsResourceDeleted(ctx context.Context, alog database.GetAuditLogsOffsetRow) bool {
switch alog.AuditLog.ResourceType {
case database.ResourceTypeTemplate: