mirror of
https://github.com/coder/coder.git
synced 2026-11-01 21:13:14 +08:00
Depends on #28394. Audit changes to seven operational agent settings through a typed `chat_operational_settings` resource. Each handler serializes baseline capture and mutation with a per-setting advisory lock, keeps both operations in one transaction, and records field-specific diffs. Effective no-op requests now skip both the database upsert and the success audit, including absent rows whose effective value is already the default. The change also retains the allowlisted raw `site_configs` query, updates generated audit and API surfaces, adds failure and concurrency coverage, and reuses the existing AuditPage resource-filter story pattern. Relates to CODAGT-720.
283 lines
9.5 KiB
Go
283 lines
9.5 KiB
Go
package coderd_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strconv"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/coder/coder/v2/coderd/audit"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
entaudit "github.com/coder/coder/v2/enterprise/audit"
|
|
"github.com/coder/coder/v2/enterprise/audit/backends"
|
|
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
|
"github.com/coder/coder/v2/enterprise/coderd/license"
|
|
"github.com/coder/coder/v2/testutil"
|
|
)
|
|
|
|
type chatOperationalSettingsAuditFixture struct {
|
|
db database.Store
|
|
client *codersdk.ExperimentalClient
|
|
ctx context.Context
|
|
systemCtx context.Context
|
|
}
|
|
|
|
type chatOperationalSettingAuditCase struct {
|
|
name string
|
|
key string
|
|
diffField string
|
|
oldValue string
|
|
oldValueAbsent bool
|
|
newValue string
|
|
effectiveDefault string
|
|
write func(context.Context, *codersdk.ExperimentalClient, string) error
|
|
}
|
|
|
|
func newChatOperationalSettingsAuditFixture(t *testing.T) chatOperationalSettingsAuditFixture {
|
|
t.Helper()
|
|
|
|
db, ps := dbtestutil.NewDB(t)
|
|
auditor := entaudit.NewAuditor(db, entaudit.DefaultFilter, backends.NewPostgres(db, true))
|
|
ownerClient, _ := coderdenttest.New(t, &coderdenttest.Options{
|
|
AuditLogging: true,
|
|
Options: &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: ps,
|
|
Auditor: auditor,
|
|
DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) {
|
|
values.Experiments = []string{string(codersdk.ExperimentChatVirtualDesktop)}
|
|
}),
|
|
},
|
|
LicenseOptions: &coderdenttest.LicenseOptions{
|
|
Features: license.Features{codersdk.FeatureAuditLog: 1},
|
|
},
|
|
})
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
return chatOperationalSettingsAuditFixture{
|
|
db: db,
|
|
client: codersdk.NewExperimentalClient(ownerClient),
|
|
ctx: ctx,
|
|
systemCtx: dbauthz.AsSystemRestricted(ctx),
|
|
}
|
|
}
|
|
|
|
func (f chatOperationalSettingsAuditFixture) auditLogs(t *testing.T) []database.GetAuditLogsOffsetRow {
|
|
t.Helper()
|
|
|
|
rows, err := f.db.GetAuditLogsOffset(f.systemCtx, database.GetAuditLogsOffsetParams{
|
|
ResourceType: string(database.ResourceTypeChatOperationalSettings),
|
|
LimitOpt: 10,
|
|
})
|
|
require.NoError(t, err)
|
|
return rows
|
|
}
|
|
|
|
func (f chatOperationalSettingsAuditFixture) onlyAuditLog(t *testing.T) database.AuditLog {
|
|
t.Helper()
|
|
|
|
rows := f.auditLogs(t)
|
|
require.Len(t, rows, 1)
|
|
return rows[0].AuditLog
|
|
}
|
|
|
|
func requireChatOperationalSettingsAuditDiff(t *testing.T, log database.AuditLog, field, oldValue, newValue string) {
|
|
t.Helper()
|
|
|
|
var diff audit.Map
|
|
require.NoError(t, json.Unmarshal(log.Diff, &diff))
|
|
require.Equal(t, audit.Map{
|
|
field: {Old: oldValue, New: newValue},
|
|
}, diff)
|
|
require.Equal(t, database.AuditActionWrite, log.Action)
|
|
require.Equal(t, database.ResourceTypeChatOperationalSettings, log.ResourceType)
|
|
require.Empty(t, log.ResourceTarget)
|
|
require.NotEqual(t, uuid.Nil, log.ResourceID)
|
|
require.Equal(t, uuid.Nil, log.OrganizationID)
|
|
require.EqualValues(t, 204, log.StatusCode)
|
|
}
|
|
|
|
func TestChatOperationalSettingsAudit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
settings := []chatOperationalSettingAuditCase{
|
|
{
|
|
name: "RetentionDays",
|
|
key: "agents_chat_retention_days",
|
|
diffField: "chat_retention_days",
|
|
oldValue: "30",
|
|
newValue: "47",
|
|
effectiveDefault: "30",
|
|
write: func(ctx context.Context, client *codersdk.ExperimentalClient, value string) error {
|
|
parsed, err := strconv.ParseInt(value, 10, 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return client.UpdateChatRetentionDays(ctx, codersdk.UpdateChatRetentionDaysRequest{RetentionDays: int32(parsed)})
|
|
},
|
|
},
|
|
{
|
|
name: "DebugRetentionDays",
|
|
key: "agents_chat_debug_retention_days",
|
|
diffField: "chat_debug_retention_days",
|
|
oldValue: "7",
|
|
newValue: "47",
|
|
effectiveDefault: strconv.FormatInt(int64(codersdk.DefaultChatDebugRetentionDays), 10),
|
|
write: func(ctx context.Context, client *codersdk.ExperimentalClient, value string) error {
|
|
parsed, err := strconv.ParseInt(value, 10, 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return client.UpdateChatDebugRetentionDays(ctx, codersdk.UpdateChatDebugRetentionDaysRequest{DebugRetentionDays: int32(parsed)})
|
|
},
|
|
},
|
|
{
|
|
name: "AutoArchiveDays",
|
|
key: "agents_chat_auto_archive_days",
|
|
diffField: "chat_auto_archive_days",
|
|
oldValue: "14",
|
|
newValue: "47",
|
|
effectiveDefault: strconv.FormatInt(int64(codersdk.DefaultChatAutoArchiveDays), 10),
|
|
write: func(ctx context.Context, client *codersdk.ExperimentalClient, value string) error {
|
|
parsed, err := strconv.ParseInt(value, 10, 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return client.UpdateChatAutoArchiveDays(ctx, codersdk.UpdateChatAutoArchiveDaysRequest{AutoArchiveDays: int32(parsed)})
|
|
},
|
|
},
|
|
{
|
|
name: "WorkspaceTTL",
|
|
key: "agents_workspace_ttl",
|
|
diffField: "workspace_ttl",
|
|
oldValue: time.Hour.String(),
|
|
newValue: (2 * time.Hour).String(),
|
|
effectiveDefault: "0s",
|
|
write: func(ctx context.Context, client *codersdk.ExperimentalClient, value string) error {
|
|
ttl, err := time.ParseDuration(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return client.UpdateChatWorkspaceTTL(ctx, codersdk.UpdateChatWorkspaceTTLRequest{
|
|
WorkspaceTTLMillis: ttl.Milliseconds(),
|
|
})
|
|
},
|
|
},
|
|
{
|
|
name: "ComputerUseProvider",
|
|
key: "agents_computer_use_provider",
|
|
diffField: "computer_use_provider",
|
|
oldValue: string(codersdk.ChatComputerUseProviderAnthropic),
|
|
oldValueAbsent: true,
|
|
newValue: string(codersdk.ChatComputerUseProviderOpenAI),
|
|
effectiveDefault: string(codersdk.ChatComputerUseProviderAnthropic),
|
|
write: func(ctx context.Context, client *codersdk.ExperimentalClient, value string) error {
|
|
return client.UpdateChatComputerUseProvider(ctx, codersdk.UpdateChatComputerUseProviderRequest{
|
|
Provider: codersdk.ChatComputerUseProvider(value),
|
|
})
|
|
},
|
|
},
|
|
{
|
|
name: "DebugLogging",
|
|
key: "agents_chat_debug_logging_allow_users",
|
|
diffField: "debug_logging_allow_users",
|
|
oldValue: "false",
|
|
newValue: "true",
|
|
effectiveDefault: "false",
|
|
write: func(ctx context.Context, client *codersdk.ExperimentalClient, value string) error {
|
|
allowUsers, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return client.UpdateChatDebugLogging(ctx, codersdk.UpdateChatDebugLoggingAllowUsersRequest{AllowUsers: allowUsers})
|
|
},
|
|
},
|
|
{
|
|
name: "PersonalModelOverrides",
|
|
key: "agents_chat_personal_model_overrides_enabled",
|
|
diffField: "personal_model_overrides_enabled",
|
|
oldValue: "false",
|
|
newValue: "true",
|
|
effectiveDefault: "false",
|
|
write: func(ctx context.Context, client *codersdk.ExperimentalClient, value string) error {
|
|
allowUsers, err := strconv.ParseBool(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return client.UpdateChatPersonalModelOverridesAdminSettings(ctx, codersdk.UpdateChatPersonalModelOverridesAdminSettingsRequest{
|
|
AllowUsers: allowUsers,
|
|
})
|
|
},
|
|
},
|
|
}
|
|
retentionDays := settings[0]
|
|
|
|
for _, setting := range settings {
|
|
t.Run(setting.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fixture := newChatOperationalSettingsAuditFixture(t)
|
|
if !setting.oldValueAbsent {
|
|
require.NoError(t, fixture.db.UpsertRuntimeConfig(fixture.systemCtx, database.UpsertRuntimeConfigParams{
|
|
Key: setting.key,
|
|
Value: setting.oldValue,
|
|
}))
|
|
}
|
|
require.NoError(t, setting.write(fixture.ctx, fixture.client, setting.newValue))
|
|
|
|
log := fixture.onlyAuditLog(t)
|
|
requireChatOperationalSettingsAuditDiff(t, log, setting.diffField, setting.oldValue, setting.newValue)
|
|
})
|
|
|
|
if setting.effectiveDefault == "" {
|
|
continue
|
|
}
|
|
t.Run(setting.name+"EffectiveDefault", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fixture := newChatOperationalSettingsAuditFixture(t)
|
|
require.NoError(t, fixture.db.DeleteRuntimeConfig(fixture.systemCtx, setting.key))
|
|
require.NoError(t, setting.write(fixture.ctx, fixture.client, setting.effectiveDefault))
|
|
|
|
stored, err := fixture.db.GetChatSiteConfigValue(fixture.systemCtx, setting.key)
|
|
require.NoError(t, err)
|
|
require.Equal(t, database.GetChatSiteConfigValueRow{}, stored)
|
|
require.Empty(t, fixture.auditLogs(t))
|
|
})
|
|
}
|
|
|
|
t.Run("InvalidWrite", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fixture := newChatOperationalSettingsAuditFixture(t)
|
|
err := fixture.client.UpdateChatRetentionDays(fixture.ctx, codersdk.UpdateChatRetentionDaysRequest{RetentionDays: -1})
|
|
require.Error(t, err)
|
|
|
|
log := fixture.onlyAuditLog(t)
|
|
require.EqualValues(t, 400, log.StatusCode)
|
|
require.JSONEq(t, "{}", string(log.Diff))
|
|
})
|
|
|
|
t.Run("MalformedRawValueRepair", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
fixture := newChatOperationalSettingsAuditFixture(t)
|
|
const malformed = "not-a-number"
|
|
require.NoError(t, fixture.db.UpsertRuntimeConfig(fixture.systemCtx, database.UpsertRuntimeConfigParams{
|
|
Key: retentionDays.key,
|
|
Value: malformed,
|
|
}))
|
|
require.NoError(t, retentionDays.write(fixture.ctx, fixture.client, "60"))
|
|
|
|
log := fixture.onlyAuditLog(t)
|
|
requireChatOperationalSettingsAuditDiff(t, log, retentionDays.diffField, malformed, "60")
|
|
})
|
|
}
|