mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
7ea0eff94e
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
227 lines
6.5 KiB
Go
227 lines
6.5 KiB
Go
package coderd
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/mock/gomock"
|
|
|
|
"cdr.dev/slog/v3/sloggers/slogtest"
|
|
"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) {
|
|
t.Parallel()
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
err error
|
|
wantDeleted bool
|
|
}{
|
|
{name: "AnError", err: assert.AnError, wantDeleted: false},
|
|
{name: "NotAuthorized", err: dbauthz.NotAuthorizedError{}, wantDeleted: false},
|
|
{name: "NoError", err: nil, wantDeleted: false},
|
|
{name: "NoRows", err: sql.ErrNoRows, wantDeleted: true},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
ctrl := gomock.NewController(t)
|
|
db := dbmock.NewMockStore(ctrl)
|
|
chatID := uuid.New()
|
|
db.EXPECT().GetChatByID(gomock.Any(), chatID).Return(database.Chat{}, tc.err)
|
|
|
|
api := &API{
|
|
Options: &Options{Database: db, Logger: slogtest.Make(t, &slogtest.Options{IgnoreErrors: true})},
|
|
}
|
|
|
|
deleted := api.auditLogIsResourceDeleted(context.Background(), database.GetAuditLogsOffsetRow{
|
|
AuditLog: database.AuditLog{ResourceType: database.ResourceTypeChat, ResourceID: chatID},
|
|
})
|
|
require.Equal(t, tc.wantDeleted, deleted)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAuditLogDescription(t *testing.T) {
|
|
t.Parallel()
|
|
testCases := []struct {
|
|
name string
|
|
alog database.GetAuditLogsOffsetRow
|
|
want string
|
|
}{
|
|
{
|
|
name: "mainline",
|
|
alog: database.GetAuditLogsOffsetRow{
|
|
AuditLog: database.AuditLog{
|
|
Action: database.AuditActionCreate,
|
|
StatusCode: 200,
|
|
ResourceType: database.ResourceTypeWorkspace,
|
|
},
|
|
},
|
|
want: "{user} created workspace {target}",
|
|
},
|
|
{
|
|
name: "unsuccessful",
|
|
alog: database.GetAuditLogsOffsetRow{
|
|
AuditLog: database.AuditLog{
|
|
Action: database.AuditActionCreate,
|
|
StatusCode: 400,
|
|
ResourceType: database.ResourceTypeWorkspace,
|
|
},
|
|
},
|
|
want: "{user} unsuccessfully attempted to create workspace {target}",
|
|
},
|
|
{
|
|
name: "login",
|
|
alog: database.GetAuditLogsOffsetRow{
|
|
AuditLog: database.AuditLog{
|
|
Action: database.AuditActionLogin,
|
|
StatusCode: 200,
|
|
ResourceType: database.ResourceTypeApiKey,
|
|
},
|
|
},
|
|
want: "{user} logged in",
|
|
},
|
|
{
|
|
name: "unsuccessful_login",
|
|
alog: database.GetAuditLogsOffsetRow{
|
|
AuditLog: database.AuditLog{
|
|
Action: database.AuditActionLogin,
|
|
StatusCode: 401,
|
|
ResourceType: database.ResourceTypeApiKey,
|
|
},
|
|
},
|
|
want: "{user} unsuccessfully attempted to login",
|
|
},
|
|
{
|
|
name: "gitsshkey",
|
|
alog: database.GetAuditLogsOffsetRow{
|
|
AuditLog: database.AuditLog{
|
|
Action: database.AuditActionDelete,
|
|
StatusCode: 200,
|
|
ResourceType: database.ResourceTypeGitSshKey,
|
|
},
|
|
},
|
|
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 {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
got := auditLogDescription(tc.alog)
|
|
require.Equal(t, tc.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
// 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,
|
|
},
|
|
}
|
|
}
|