mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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:
|
||||
|
||||
@@ -3,6 +3,7 @@ package coderd
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -14,6 +15,7 @@ import (
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbmock"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
func TestAuditLogIsResourceDeleted(t *testing.T) {
|
||||
@@ -111,6 +113,91 @@ func TestAuditLogDescription(t *testing.T) {
|
||||
},
|
||||
want: "{user} deleted the git ssh key",
|
||||
},
|
||||
{
|
||||
name: "chat_archived",
|
||||
alog: chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"archived": {Old: false, New: true},
|
||||
}),
|
||||
want: "{user} archived chat {target}",
|
||||
},
|
||||
{
|
||||
name: "chat_unarchived",
|
||||
alog: chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"archived": {Old: true, New: false},
|
||||
}),
|
||||
want: "{user} unarchived chat {target}",
|
||||
},
|
||||
{
|
||||
name: "chat_sharing_user_acl",
|
||||
alog: chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"user_acl": {Old: map[string]any{}, New: map[string]any{"user-1": map[string]any{"permissions": []string{"read"}}}},
|
||||
}),
|
||||
want: "{user} updated sharing for chat {target}",
|
||||
},
|
||||
{
|
||||
name: "chat_sharing_group_acl",
|
||||
alog: chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"group_acl": {Old: map[string]any{}, New: map[string]any{"group-1": map[string]any{"permissions": []string{"read"}}}},
|
||||
}),
|
||||
want: "{user} updated sharing for chat {target}",
|
||||
},
|
||||
{
|
||||
name: "chat_sharing_both_acls",
|
||||
alog: chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"user_acl": {Old: map[string]any{}, New: map[string]any{"user-1": map[string]any{"permissions": []string{"read"}}}},
|
||||
"group_acl": {Old: map[string]any{}, New: map[string]any{"group-1": map[string]any{"permissions": []string{"read"}}}},
|
||||
}),
|
||||
want: "{user} updated sharing for chat {target}",
|
||||
},
|
||||
{
|
||||
name: "chat_mixed_diff_falls_through",
|
||||
alog: chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"archived": {Old: false, New: true},
|
||||
"pin_order": {Old: 1, New: 0},
|
||||
}),
|
||||
want: "{user} updated chat {target}",
|
||||
},
|
||||
{
|
||||
name: "chat_acl_with_extra_field_falls_through",
|
||||
alog: chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"user_acl": {Old: map[string]any{}, New: map[string]any{}},
|
||||
"pin_order": {Old: 1, New: 0},
|
||||
}),
|
||||
want: "{user} updated chat {target}",
|
||||
},
|
||||
{
|
||||
name: "chat_failed_write_no_override",
|
||||
alog: func() database.GetAuditLogsOffsetRow {
|
||||
row := chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"archived": {Old: false, New: true},
|
||||
})
|
||||
row.AuditLog.StatusCode = 400
|
||||
return row
|
||||
}(),
|
||||
want: "{user} unsuccessfully attempted to write chat {target}",
|
||||
},
|
||||
{
|
||||
name: "chat_redirect_no_override",
|
||||
alog: func() database.GetAuditLogsOffsetRow {
|
||||
row := chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"archived": {Old: false, New: true},
|
||||
})
|
||||
row.AuditLog.StatusCode = 303
|
||||
return row
|
||||
}(),
|
||||
want: "{user} was redirected attempting to write chat {target}",
|
||||
},
|
||||
{
|
||||
name: "chat_non_write_action_no_override",
|
||||
alog: func() database.GetAuditLogsOffsetRow {
|
||||
row := chatAuditLogRow(t, codersdk.AuditDiff{
|
||||
"user_acl": {Old: map[string]any{}, New: map[string]any{"user-1": map[string]any{"permissions": []string{"read"}}}},
|
||||
})
|
||||
row.AuditLog.Action = database.AuditActionCreate
|
||||
return row
|
||||
}(),
|
||||
want: "{user} created chat {target}",
|
||||
},
|
||||
}
|
||||
// nolint: paralleltest // no longer need to reinitialize loop vars in go 1.22
|
||||
for _, tc := range testCases {
|
||||
@@ -121,3 +208,19 @@ func TestAuditLogDescription(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// chatAuditLogRow builds a GetAuditLogsOffsetRow for a successful chat write
|
||||
// with the given diff, suitable for testing auditLogDescription.
|
||||
func chatAuditLogRow(t *testing.T, diff codersdk.AuditDiff) database.GetAuditLogsOffsetRow {
|
||||
t.Helper()
|
||||
rawDiff, err := json.Marshal(diff)
|
||||
require.NoError(t, err)
|
||||
return database.GetAuditLogsOffsetRow{
|
||||
AuditLog: database.AuditLog{
|
||||
Action: database.AuditActionWrite,
|
||||
StatusCode: 200,
|
||||
ResourceType: database.ResourceTypeChat,
|
||||
Diff: rawDiff,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user